Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/vertex/app/(authenticated)/admin/sites/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { AqArrowLeft } from "@airqo/icons-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { useSiteDetails, useRefreshSiteMetadata } from "@/core/hooks/useSites";
import { useSiteDetails } from "@/core/hooks/useSites";
import { useRefreshMetadataWithBanner } from "@/core/hooks/useRefreshMetadataWithBanner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { useParams } from "next/navigation";
Expand All @@ -29,7 +30,7 @@ export default function SiteDetailsPage() {
const params = useParams();
const siteId = params.id as string;
const { data: site, isLoading, error } = useSiteDetails(siteId);
const { mutate: refreshMetadata, isPending: isRefreshing } = useRefreshSiteMetadata();
const { mutate: refreshMetadata, isPending: isRefreshing } = useRefreshMetadataWithBanner();
const router = useRouter();
const [editSection, setEditSection] = useState<"general" | "mobile" | null>(
null
Expand Down
5 changes: 3 additions & 2 deletions src/vertex/app/(authenticated)/sites/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { AqArrowLeft } from "@airqo/icons-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { useSiteDetails, useRefreshSiteMetadata } from "@/core/hooks/useSites";
import { useSiteDetails } from "@/core/hooks/useSites";
import { useRefreshMetadataWithBanner } from "@/core/hooks/useRefreshMetadataWithBanner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { useParams } from "next/navigation";
Expand All @@ -31,7 +32,7 @@ export default function UserSiteDetailsPage() {
const params = useParams();
const siteId = params.id as string;
const { data: site, isLoading, error } = useSiteDetails(siteId);
const { mutate: refreshMetadata, isPending: isRefreshing } = useRefreshSiteMetadata();
const { mutate: refreshMetadata, isPending: isRefreshing } = useRefreshMetadataWithBanner();
const router = useRouter();
const [editSection, setEditSection] = useState<"general" | "mobile" | null>(
null
Expand Down
33 changes: 33 additions & 0 deletions src/vertex/app/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@

---

## Version 2.0.2
**Released:** June 10, 2026

### Site Management Banner Migration

Migrated all user-facing notifications in the Site Management module from `ReusableToast` and Sonner `toast` to the centralized `useBanner` / `useBannerWithDelay` / `useClipboard` system.

<details>
<summary><strong>Changes (4)</strong></summary>

- **Hooks Decoupled**: Removed `ReusableToast` from 4 mutation hooks in `useSites.ts` — `useApproximateCoordinates`, `useUpdateSiteDetails`, `useCreateSite`, `useRefreshSiteMetadata` — and replaced with optional `onSuccess`/`onError` callback interfaces. Cache invalidation logic stays in the hooks; notification responsibility is delegated to the UI layer.
- **Create & Edit Dialogs**: `create-site-form.tsx` wires `useCreateSite` and `useApproximateCoordinates` hook-level callbacks — errors use `scoped: true` (inline in dialog), site creation success uses `showBannerWithDelay` (`scoped: false`) after navigation fires. `edit-site-details-dialog.tsx` replaces 2 Sonner `toast.error` calls with `showBanner` (`scoped: true`) and routes mutation success/error through hook-level callbacks.
- **Site Detail Pages**: Both `admin/sites/[id]/page.tsx` and `sites/[id]/page.tsx` wire `useRefreshSiteMetadata` with severity-aware banners (`scoped: false`): `warning` for partial refresh, `info` for already-complete, `success` for full refresh. The duplicated banner logic has been extracted into a shared `useRefreshMetadataWithBanner` hook.
- **In-Page Copy Actions**: `site-information-card.tsx` and `site-measurements-api-card.tsx` replace clipboard `ReusableToast` calls with the shared `useClipboard` hook, adding async error handling for free.

</details>

<details>
<summary><strong>Files Updated (8)</strong></summary>

- `src/vertex/core/hooks/useSites.ts` [MODIFIED]
- `src/vertex/components/features/sites/create-site-form.tsx` [MODIFIED]
- `src/vertex/components/features/sites/edit-site-details-dialog.tsx` [MODIFIED]
- `src/vertex/components/features/sites/site-information-card.tsx` [MODIFIED]
- `src/vertex/components/features/sites/site-measurements-api-card.tsx` [MODIFIED]
- `src/vertex/app/(authenticated)/admin/sites/[id]/page.tsx` [MODIFIED]
- `src/vertex/app/(authenticated)/sites/[id]/page.tsx` [MODIFIED]
- `src/vertex/core/hooks/useRefreshMetadataWithBanner.ts` [ADDED]

</details>

---

## Version 2.0.1
**Released:** June 09, 2026

Expand Down
20 changes: 18 additions & 2 deletions src/vertex/components/features/sites/create-site-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import ReusableButton from "@/components/shared/button/ReusableButton";
import { useAppSelector } from "@/core/redux/hooks";
import "leaflet/dist/leaflet.css";
import { useApproximateCoordinates, useCreateSite } from "@/core/hooks/useSites";
import { useBanner } from "@/context/banner-context";
import { useBannerWithDelay } from "@/core/hooks/useBannerWithDelay";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { AqPlus } from "@airqo/icons-react";
import LocationAutocomplete from "@/components/features/location-autocomplete/LocationAutocomplete";
import { Label } from "@/components/ui/label";
Expand Down Expand Up @@ -68,8 +71,21 @@ export function CreateSiteForm({ disabled = false, basePath = "/admin/sites" }:
const router = useRouter();
const [inputMode, setInputMode] = useState<"siteName" | "coordinates">("coordinates");
const activeGroup = useAppSelector((state) => state.user.activeGroup);
const { getApproximateCoordinates, isPending: isOptimizing } = useApproximateCoordinates();
const { mutate: createSite, isPending: isCreating } = useCreateSite();
const { showBanner } = useBanner();
const { showBannerWithDelay } = useBannerWithDelay();
const { getApproximateCoordinates, isPending: isOptimizing } = useApproximateCoordinates({
onError: (error) => {
showBanner({ severity: 'error', message: `Unable to get approximate coordinates: ${getApiErrorMessage(error)}`, scoped: true });
},
});
const { mutate: createSite, isPending: isCreating } = useCreateSite({
onSuccess: (_data, variables) => {
showBannerWithDelay({ severity: 'success', message: `Site '${variables.name}' created successfully`, scoped: false });
},
onError: (error) => {
showBanner({ severity: 'error', message: `Failed to create site: ${getApiErrorMessage(error)}`, scoped: true });
},
Comment on lines +78 to +87
});
const { networks, isLoading: isLoadingNetworks } = useNetworks();

const form = useForm<SiteFormValues>({
Expand Down
19 changes: 15 additions & 4 deletions src/vertex/components/features/sites/edit-site-details-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { useForm } from "react-hook-form";
import * as z from "zod";
import { Form, FormField } from "@/components/ui/form";
import { useUpdateSiteDetails } from "@/core/hooks/useSites";
import { toast } from "sonner";
import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { useBanner } from "@/context/banner-context";
import { useBannerWithDelay } from "@/core/hooks/useBannerWithDelay";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { useEffect } from "react";
import { Site } from "@/app/types/sites";

Expand Down Expand Up @@ -49,7 +51,16 @@ export function EditSiteDetailsDialog({
site,
section,
}: EditSiteDetailsDialogProps) {
const { mutate: updateSite, isPending } = useUpdateSiteDetails();
const { showBanner } = useBanner();
const { showBannerWithDelay } = useBannerWithDelay();
const { mutate: updateSite, isPending } = useUpdateSiteDetails({
onSuccess: () => {
showBannerWithDelay({ severity: 'success', message: 'Site details updated successfully', scoped: false });
},
onError: (error) => {
showBanner({ severity: 'error', message: `Failed to update site: ${getApiErrorMessage(error)}`, scoped: true });
},
Comment on lines +58 to +62
});

const form = useForm<SiteFormValues>({
resolver: zodResolver(siteFormSchema),
Expand Down Expand Up @@ -107,12 +118,12 @@ export function EditSiteDetailsDialog({
);

if (Object.keys(transformedData).length === 0) {
toast.error("No fields have been modified");
showBanner({ severity: 'error', message: 'No fields have been modified', scoped: true });
return;
}

if (!site._id) {
toast.error("Site ID is missing");
showBanner({ severity: 'error', message: 'Site ID is missing', scoped: true });
return;
}

Expand Down
15 changes: 3 additions & 12 deletions src/vertex/components/features/sites/site-information-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@/components/ui/tooltip";
import { AqEdit01, AqCopy01 } from "@airqo/icons-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useClipboard } from "@/core/hooks/useClipboard";
import {
badgeColorClasses,
formatDisplayDate,
Expand All @@ -33,6 +33,7 @@ const DetailItem = ({ label, value }: { label: string; value: React.ReactNode })
);

export const SiteInformationCard: React.FC<SiteInformationCardProps> = ({ site, onEdit }) => {
const { handleCopy } = useClipboard();
const lastActiveCheck = site.lastActive
? formatDisplayDate(site.lastActive)
: null;
Expand Down Expand Up @@ -67,17 +68,7 @@ export const SiteInformationCard: React.FC<SiteInformationCardProps> = ({ site,
<span className="font-mono text-sm truncate max-w-[150px] sm:max-w-none">{site._id}</span>
<ReusableButton
variant="text"
onClick={async () => {
if (site._id) {
try {
await navigator.clipboard.writeText(site._id);
ReusableToast({ message: "Copied", type: "SUCCESS" });
} catch (error) {
console.error("Failed to copy:", error);
ReusableToast({ message: "Failed to copy", type: "ERROR" });
}
}
}}
onClick={() => site._id && handleCopy(site._id)}
className="p-1 h-auto hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
Icon={AqCopy01}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"use client";

import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Copy } from "lucide-react";
import React from "react";
import ReusableToast from "@/components/shared/toast/ReusableToast";
import { useClipboard } from "@/core/hooks/useClipboard";

interface SiteMeasurementsApiCardProps {
siteId: string;
}

const SiteMeasurementsApiCard: React.FC<SiteMeasurementsApiCardProps> = ({ siteId }) => {
const { handleCopy } = useClipboard();

return (
<Card className="w-full rounded-lg flex flex-col gap-4 px-3 py-2">
<h2 className="text-lg font-semibold mb-2">Site Measurements API</h2>
Expand All @@ -22,11 +26,9 @@ const SiteMeasurementsApiCard: React.FC<SiteMeasurementsApiCardProps> = ({ siteI
<Button
variant="ghost"
size="icon"
aria-label="Copy recent measurements API URL"
className="hover:bg-transparent"
onClick={() => {
navigator.clipboard.writeText(`https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/recent?token=YOUR_TOKEN`);
ReusableToast({ message: "Copied", type: "SUCCESS" });
}}
onClick={() => handleCopy(`https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/recent?token=YOUR_TOKEN`)}
>
<Copy className="w-4 h-4" />
</Button>
Comment on lines +31 to 34
Expand All @@ -42,11 +44,9 @@ const SiteMeasurementsApiCard: React.FC<SiteMeasurementsApiCardProps> = ({ siteI
<Button
variant="ghost"
size="icon"
aria-label="Copy historical measurements API URL"
className="hover:bg-transparent"
onClick={() => {
navigator.clipboard.writeText(`https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/historical?token=YOUR_TOKEN`);
ReusableToast({ message: "Copied", type: "SUCCESS" });
}}
onClick={() => handleCopy(`https://api.airqo.net/api/v2/devices/measurements/sites/${siteId}/historical?token=YOUR_TOKEN`)}
>
<Copy className="w-4 h-4" />
</Button>
Expand Down
34 changes: 34 additions & 0 deletions src/vertex/core/hooks/useRefreshMetadataWithBanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useCallback } from "react";
import { useRefreshSiteMetadata } from "@/core/hooks/useSites";
import { useBanner } from "@/context/banner-context";
Comment on lines +1 to +3

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot Hooks can only be called from client components which use use client.

import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import type { SiteRefreshResponse } from "@/core/apis/sites";
import type { AxiosError } from "axios";

interface ErrorResponse {
message: string;
errors?: {
message: string;
};
}
Comment on lines +8 to +13

export const useRefreshMetadataWithBanner = () => {
const { showBanner } = useBanner();

const handleSuccess = useCallback((data: SiteRefreshResponse) => {
const msg = (data.message ?? "").toLowerCase();
if (msg.includes("partially refreshed")) {
showBanner({ severity: "warning", message: data.message ?? "Site metadata partially refreshed.", scoped: false });
} else if (msg.includes("already complete")) {
showBanner({ severity: "info", message: "Site metadata is already up to date.", scoped: false });
} else {
showBanner({ severity: "success", message: "Site metadata refreshed successfully.", scoped: false });
}
}, [showBanner]);
Comment on lines +18 to +27

const handleError = useCallback((error: AxiosError<ErrorResponse>) => {
showBanner({ severity: "error", message: `Refresh Failed: ${getApiErrorMessage(error)}`, scoped: false });
}, [showBanner]);

return useRefreshSiteMetadata({ onSuccess: handleSuccess, onError: handleError });
};
Comment on lines +15 to +34
Loading
Loading