Updated loading states and error states accross app
This commit is contained in:
@@ -3,6 +3,8 @@ import type { ZoomInOptions } from "../../types/types";
|
|||||||
import NavigationArrow from "../UI/NavigationArrow";
|
import NavigationArrow from "../UI/NavigationArrow";
|
||||||
import { useCameraZoom } from "../../hooks/useCameraZoom";
|
import { useCameraZoom } from "../../hooks/useCameraZoom";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import Loading from "../UI/Loading";
|
||||||
|
import ErrorState from "../UI/ErrorState";
|
||||||
|
|
||||||
type SnapshotContainerProps = {
|
type SnapshotContainerProps = {
|
||||||
side: string;
|
side: string;
|
||||||
@@ -42,13 +44,16 @@ export const SnapshotContainer = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [zoomLevel]);
|
}, [zoomLevel]);
|
||||||
|
|
||||||
if (isError) return <p className="h-100">An error occurred</p>;
|
|
||||||
if (isPending) return <p className="h-100">Loading...</p>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row">
|
<div className="flex flex-col md:flex-row">
|
||||||
<NavigationArrow side={side} settingsPage={settingsPage} />
|
<NavigationArrow side={side} settingsPage={settingsPage} />
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
{isError && <ErrorState />}
|
||||||
|
{isPending && (
|
||||||
|
<div className="my-50 h-[50%]">
|
||||||
|
<Loading message="Camera Preview" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<canvas
|
<canvas
|
||||||
onClick={handleZoomClick}
|
onClick={handleZoomClick}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type CameraSettingsProps = {
|
|||||||
updateCameraConfig: (values: CameraSettingValues) => Promise<void> | void;
|
updateCameraConfig: (values: CameraSettingValues) => Promise<void> | void;
|
||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
onZoomLevelChange?: (level: number) => void;
|
onZoomLevelChange?: (level: number) => void;
|
||||||
|
updateCameraConfigError: null | Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CameraSettingFields = ({
|
const CameraSettingFields = ({
|
||||||
@@ -24,6 +25,7 @@ const CameraSettingFields = ({
|
|||||||
updateCameraConfig,
|
updateCameraConfig,
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
onZoomLevelChange,
|
onZoomLevelChange,
|
||||||
|
updateCameraConfigError,
|
||||||
}: CameraSettingsProps) => {
|
}: CameraSettingsProps) => {
|
||||||
const [showPwd, setShowPwd] = useState(false);
|
const [showPwd, setShowPwd] = useState(false);
|
||||||
const cameraControllerSide =
|
const cameraControllerSide =
|
||||||
@@ -43,23 +45,20 @@ const CameraSettingFields = ({
|
|||||||
switch (levelstring) {
|
switch (levelstring) {
|
||||||
case "1x":
|
case "1x":
|
||||||
return 1;
|
return 1;
|
||||||
break;
|
|
||||||
case "2x":
|
case "2x":
|
||||||
return 2;
|
return 2;
|
||||||
break;
|
|
||||||
case "4x":
|
case "4x":
|
||||||
return 4;
|
return 4;
|
||||||
break;
|
|
||||||
case "8x":
|
case "8x":
|
||||||
return 8;
|
return 8;
|
||||||
default:
|
default:
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const level = getZoomLevel(query.data);
|
|
||||||
|
|
||||||
console.log("level from get", level);
|
|
||||||
console.log("zoomLevel state", zoomLevel);
|
|
||||||
const initialValues = useMemo<CameraSettingValues>(
|
const initialValues = useMemo<CameraSettingValues>(
|
||||||
() => ({
|
() => ({
|
||||||
friendlyName: initialData?.id ?? "",
|
friendlyName: initialData?.id ?? "",
|
||||||
@@ -70,6 +69,7 @@ const CameraSettingFields = ({
|
|||||||
|
|
||||||
zoom: zoomLevel,
|
zoom: zoomLevel,
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[initialData?.id, initialData?.propURI?.value, zoomLevel]
|
[initialData?.id, initialData?.propURI?.value, zoomLevel]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -96,7 +96,6 @@ const CameraSettingFields = ({
|
|||||||
mutation.mutate(zoomInOptions);
|
mutation.mutate(zoomInOptions);
|
||||||
};
|
};
|
||||||
const selectedZoom = zoomLevel ?? 1;
|
const selectedZoom = zoomLevel ?? 1;
|
||||||
console.log(selectedZoom);
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
@@ -105,7 +104,7 @@ const CameraSettingFields = ({
|
|||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ errors, touched }) => (
|
{({ errors, touched, isSubmitting }) => (
|
||||||
<Form className="flex flex-col space-y-6 p-2">
|
<Form className="flex flex-col space-y-6 p-2">
|
||||||
<div className="flex flex-col space-y-2 relative">
|
<div className="flex flex-col space-y-2 relative">
|
||||||
<label htmlFor="friendlyName">Name</label>
|
<label htmlFor="friendlyName">Name</label>
|
||||||
@@ -184,7 +183,7 @@ const CameraSettingFields = ({
|
|||||||
<CardHeader title="Zoom settings" />
|
<CardHeader title="Zoom settings" />
|
||||||
<div className="mx-auto grid grid-cols-4 items-center">
|
<div className="mx-auto grid grid-cols-4 items-center">
|
||||||
{zoomOptions.map((zoom) => (
|
{zoomOptions.map((zoom) => (
|
||||||
<div key={zoom}>
|
<div key={zoom} className="my-3">
|
||||||
<Field
|
<Field
|
||||||
type="radio"
|
type="radio"
|
||||||
name="zoom"
|
name="zoom"
|
||||||
@@ -206,12 +205,20 @@ const CameraSettingFields = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
{updateCameraConfigError ? (
|
||||||
|
<button className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-[#26B170] text-white rounded-lg p-2 mx-auto h-[100%] w-full"
|
className="bg-[#26B170] text-white rounded-lg p-2 mx-auto h-[100%] w-full"
|
||||||
>
|
>
|
||||||
Save settings
|
{isSubmitting ? "Saving" : "Save settings"}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useFetchCameraConfig } from "../../hooks/useCameraConfig";
|
import { useFetchCameraConfig } from "../../hooks/useCameraConfig";
|
||||||
|
|
||||||
import Card from "../UI/Card";
|
import Card from "../UI/Card";
|
||||||
import CardHeader from "../UI/CardHeader";
|
import CardHeader from "../UI/CardHeader";
|
||||||
import CameraSettingFields from "./CameraSettingFields";
|
import CameraSettingFields from "./CameraSettingFields";
|
||||||
@@ -16,22 +15,23 @@ const CameraSettings = ({
|
|||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
onZoomLevelChange?: (level: number) => void;
|
onZoomLevelChange?: (level: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isError, isPending, updateCameraConfig } =
|
const { data, updateCameraConfig, updateCameraConfigError } =
|
||||||
useFetchCameraConfig(side);
|
useFetchCameraConfig(side);
|
||||||
|
console.log(updateCameraConfigError);
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[40%] p-4">
|
<Card className="overflow-hidden min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[40%] p-4">
|
||||||
{isPending && <>Loading camera config</>}
|
|
||||||
{isError && <>Error fetching camera config</>}
|
|
||||||
<div className="relative flex flex-col space-y-3">
|
<div className="relative flex flex-col space-y-3">
|
||||||
<CardHeader title={title} icon={faWrench} />
|
<CardHeader title={title} icon={faWrench} />
|
||||||
{!isPending && (
|
|
||||||
|
{
|
||||||
<CameraSettingFields
|
<CameraSettingFields
|
||||||
initialData={data}
|
initialData={data}
|
||||||
updateCameraConfig={updateCameraConfig}
|
updateCameraConfig={updateCameraConfig}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
onZoomLevelChange={onZoomLevelChange}
|
onZoomLevelChange={onZoomLevelChange}
|
||||||
|
updateCameraConfigError={updateCameraConfigError}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type AlertItemProps = {
|
|||||||
|
|
||||||
const AlertItem = ({ item }: AlertItemProps) => {
|
const AlertItem = ({ item }: AlertItemProps) => {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const { dispatch } = useAlertHitContext();
|
const { dispatch, isError } = useAlertHitContext();
|
||||||
|
|
||||||
// const {d} = useCameraBlackboard();
|
// const {d} = useCameraBlackboard();
|
||||||
const motionAway = (item?.motion ?? "").toUpperCase() === "AWAY";
|
const motionAway = (item?.motion ?? "").toUpperCase() === "AWAY";
|
||||||
@@ -24,6 +24,7 @@ const AlertItem = ({ item }: AlertItemProps) => {
|
|||||||
const isNPEDHitB = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "B";
|
const isNPEDHitB = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "B";
|
||||||
const isNPEDHitC = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "C";
|
const isNPEDHitC = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "C";
|
||||||
|
|
||||||
|
console.log(isError);
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const NPEDFields = () => {
|
|||||||
...values,
|
...values,
|
||||||
};
|
};
|
||||||
signIn(valuesToSend);
|
signIn(valuesToSend);
|
||||||
toast.success("Signed into NPED Successfully");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateValues = (values: NPEDFieldType) => {
|
const validateValues = (values: NPEDFieldType) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { toast } from "sonner";
|
||||||
import type { SystemValues } from "../../../types/types";
|
import type { SystemValues } from "../../../types/types";
|
||||||
import { CAM_BASE } from "../../../utils/config";
|
import { CAM_BASE } from "../../../utils/config";
|
||||||
|
|
||||||
@@ -35,7 +36,13 @@ export async function handleSystemSave(values: SystemValues) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
toast.error(`Failed to save system settings: ${err.message}`);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
} else {
|
||||||
|
toast.error("An unexpected error occurred while saving.");
|
||||||
|
console.error("Unknown error:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +86,12 @@ export async function handleSystemRecall() {
|
|||||||
|
|
||||||
return { deviceName, sntpServer, sntpInterval, timeZone };
|
return { deviceName, sntpServer, sntpInterval, timeZone };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
if (err instanceof Error) {
|
||||||
|
toast.error(`Error: ${err.message}`);
|
||||||
|
} else {
|
||||||
|
toast.error("An unexpected error occurred");
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import type { SystemValues, SystemValuesErrors } from "../../../types/types";
|
|||||||
import { useSystemConfig } from "../../../hooks/useSystemConfig";
|
import { useSystemConfig } from "../../../hooks/useSystemConfig";
|
||||||
|
|
||||||
const SystemConfigFields = () => {
|
const SystemConfigFields = () => {
|
||||||
const { saveSystemSettings, systemSettingsData } = useSystemConfig();
|
const { saveSystemSettings, systemSettingsData, saveSystemSettingsLoading } =
|
||||||
|
useSystemConfig();
|
||||||
const initialvalues: SystemValues = {
|
const initialvalues: SystemValues = {
|
||||||
deviceName: systemSettingsData?.deviceName ?? "",
|
deviceName: systemSettingsData?.deviceName ?? "",
|
||||||
timeZone: systemSettingsData?.timeZone ?? "",
|
timeZone: systemSettingsData?.timeZone ?? "",
|
||||||
@@ -37,7 +38,7 @@ const SystemConfigFields = () => {
|
|||||||
validateOnChange
|
validateOnChange
|
||||||
validateOnBlur
|
validateOnBlur
|
||||||
>
|
>
|
||||||
{({ values, errors, touched }) => (
|
{({ values, errors, touched, isSubmitting }) => (
|
||||||
<Form className="flex flex-col space-y-5 px-2">
|
<Form className="flex flex-col space-y-5 px-2">
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label
|
<label
|
||||||
@@ -131,8 +132,9 @@ const SystemConfigFields = () => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full md:w-[50%]"
|
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full md:w-[50%]"
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Save System Settings
|
{saveSystemSettingsLoading ? "Saving..." : "Save System Settings"}
|
||||||
</button>
|
</button>
|
||||||
<SystemFileUpload
|
<SystemFileUpload
|
||||||
name={"softwareUpdate"}
|
name={"softwareUpdate"}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { ModemSettingsType } from "../../../types/types";
|
|||||||
import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem";
|
import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ModemToggle from "./ModemToggle";
|
import ModemToggle from "./ModemToggle";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
const ModemSettings = () => {
|
const ModemSettings = () => {
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
@@ -50,11 +49,6 @@ const ModemSettings = () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
modemMutation.mutate(modemConfig);
|
modemMutation.mutate(modemConfig);
|
||||||
if (modemMutation.error) {
|
|
||||||
toast.error("Failed to update modem settings");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.success("Modem settings updated");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Field, Form, Formik } from "formik";
|
|||||||
import FormGroup from "../components/FormGroup";
|
import FormGroup from "../components/FormGroup";
|
||||||
import type { WifiSettingValues } from "../../../types/types";
|
import type { WifiSettingValues } from "../../../types/types";
|
||||||
import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem";
|
import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
|
import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -36,12 +35,6 @@ const WiFiSettingsForm = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
wifiMutation.mutate(wifiConfig);
|
wifiMutation.mutate(wifiConfig);
|
||||||
|
|
||||||
if (wifiMutation.error) {
|
|
||||||
toast.error("Failed to update WiFi settings");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.success("WiFi settings updated");
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSightingFeedContext } from "../../context/SightingFeedContext";
|
|||||||
import { useHiDPICanvas } from "../../hooks/useHiDPICanvas";
|
import { useHiDPICanvas } from "../../hooks/useHiDPICanvas";
|
||||||
import NavigationArrow from "../UI/NavigationArrow";
|
import NavigationArrow from "../UI/NavigationArrow";
|
||||||
import NumberPlate from "../PlateStack/NumberPlate";
|
import NumberPlate from "../PlateStack/NumberPlate";
|
||||||
|
import Loading from "../UI/Loading";
|
||||||
|
|
||||||
const SightingOverview = () => {
|
const SightingOverview = () => {
|
||||||
const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0);
|
const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0);
|
||||||
@@ -22,18 +23,11 @@ const SightingOverview = () => {
|
|||||||
|
|
||||||
const { sync } = useHiDPICanvas(imgRef, canvasRef);
|
const { sync } = useHiDPICanvas(imgRef, canvasRef);
|
||||||
|
|
||||||
if (!mostRecent)
|
|
||||||
return (
|
|
||||||
<div className="h-150 flex items-center justify-center text-3xl animate-pulse">
|
|
||||||
<NavigationArrow side={side} />
|
|
||||||
<p>No Recent Sightings</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return (
|
return (
|
||||||
<div className="h-150 flex items-center justify-center text-3xl animate-pulse">
|
<div className="h-150 flex items-center justify-center">
|
||||||
<NavigationArrow side={side} />
|
<NavigationArrow side={side} />
|
||||||
<p>Loading</p>
|
<Loading message="Loading" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,6 +36,14 @@ const SightingOverview = () => {
|
|||||||
An error occurred. Cannot display footage.
|
An error occurred. Cannot display footage.
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
if (!mostRecent)
|
||||||
|
return (
|
||||||
|
<div className="h-150 flex items-center justify-center text-3xl animate-pulse">
|
||||||
|
<NavigationArrow side={side} />
|
||||||
|
<Loading message="No Recent Sightings" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row">
|
<div className="flex flex-col md:flex-row">
|
||||||
<NavigationArrow side={side} />
|
<NavigationArrow side={side} />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import popup from "../../assets/sounds/ui/popup_open.mp3";
|
|||||||
import { useSound } from "react-sounds";
|
import { useSound } from "react-sounds";
|
||||||
import { useNPEDContext } from "../../context/NPEDUserContext";
|
import { useNPEDContext } from "../../context/NPEDUserContext";
|
||||||
import { useSoundContext } from "../../context/SoundContext";
|
import { useSoundContext } from "../../context/SoundContext";
|
||||||
|
import Loading from "../UI/Loading";
|
||||||
|
|
||||||
function useNow(tickMs = 1000) {
|
function useNow(tickMs = 1000) {
|
||||||
const [, setNow] = useState(() => Date.now());
|
const [, setNow] = useState(() => Date.now());
|
||||||
@@ -54,6 +55,7 @@ export default function SightingHistoryWidget({
|
|||||||
isSightingModalOpen,
|
isSightingModalOpen,
|
||||||
selectedSighting,
|
selectedSighting,
|
||||||
mostRecent,
|
mostRecent,
|
||||||
|
isLoading,
|
||||||
} = useSightingFeedContext();
|
} = useSightingFeedContext();
|
||||||
|
|
||||||
const { dispatch } = useAlertHitContext();
|
const { dispatch } = useAlertHitContext();
|
||||||
@@ -64,6 +66,7 @@ export default function SightingHistoryWidget({
|
|||||||
if (!mostRecent) return;
|
if (!mostRecent) return;
|
||||||
setSessionList([...sessionList, mostRecent]);
|
setSessionList([...sessionList, mostRecent]);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [mostRecent, sessionStarted, setSessionList]);
|
}, [mostRecent, sessionStarted, setSessionList]);
|
||||||
|
|
||||||
const hasAutoOpenedRef = useRef(false);
|
const hasAutoOpenedRef = useRef(false);
|
||||||
@@ -127,6 +130,11 @@ export default function SightingHistoryWidget({
|
|||||||
>
|
>
|
||||||
<CardHeader title={title} />
|
<CardHeader title={title} />
|
||||||
<div className="flex flex-col gap-3 ">
|
<div className="flex flex-col gap-3 ">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="my-50 h-[50%]">
|
||||||
|
<Loading message="Loading Sightings" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{rows?.map((obj) => {
|
{rows?.map((obj) => {
|
||||||
|
|||||||
162
src/components/UI/ErrorState.tsx
Normal file
162
src/components/UI/ErrorState.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faTriangleExclamation,
|
||||||
|
faRotateRight,
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
|
faClipboard,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useState, type FC } from "react";
|
||||||
|
|
||||||
|
type Variant = "inline" | "card" | "banner";
|
||||||
|
|
||||||
|
export type ErrorStateProps = {
|
||||||
|
/** Main heading shown to the user */
|
||||||
|
title?: string;
|
||||||
|
/** Friendly message for the user */
|
||||||
|
message?: string;
|
||||||
|
/** Raw error to help devs (object, string, whatever) */
|
||||||
|
error?: unknown;
|
||||||
|
/** Called when user clicks Retry */
|
||||||
|
onRetry?: () => Promise<void> | void;
|
||||||
|
/** Show a Retry button */
|
||||||
|
showRetry?: boolean;
|
||||||
|
/** Optional custom icon */
|
||||||
|
icon?: IconDefinition;
|
||||||
|
/** Visual style */
|
||||||
|
variant?: Variant;
|
||||||
|
/** Additional actions (e.g. “Report”) */
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
/** Test id for testing */
|
||||||
|
"data-testid"?: string;
|
||||||
|
/** ClassName passthrough */
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatError(err: unknown) {
|
||||||
|
if (!err) return "";
|
||||||
|
if (typeof err === "string") return err;
|
||||||
|
if (err instanceof Error) return err.stack || err.message;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(err, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseStyles = "w-full text-left flex items-start gap-3 rounded-md border";
|
||||||
|
|
||||||
|
const variants: Record<Variant, string> = {
|
||||||
|
inline: "p-3 border-red-200 bg-red-50 text-red-800",
|
||||||
|
card: "p-4 border-red-200 bg-red-50 text-red-800 shadow-sm",
|
||||||
|
banner: "p-3 border-red-200 bg-red-50 text-red-800 rounded-none border-x-0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorState: FC<ErrorStateProps> = ({
|
||||||
|
title = "Something went wrong",
|
||||||
|
message = "Please try again or contact support if the problem persists.",
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
showRetry = !!onRetry,
|
||||||
|
icon = faTriangleExclamation,
|
||||||
|
variant = "inline",
|
||||||
|
actions,
|
||||||
|
className = "",
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [retrying, setRetrying] = useState(false);
|
||||||
|
|
||||||
|
const details = formatError(error);
|
||||||
|
|
||||||
|
async function handleRetry() {
|
||||||
|
if (!onRetry) return;
|
||||||
|
try {
|
||||||
|
setRetrying(true);
|
||||||
|
await onRetry();
|
||||||
|
} finally {
|
||||||
|
setRetrying(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDetails() {
|
||||||
|
if (!details) return;
|
||||||
|
navigator.clipboard?.writeText(details).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
className={`${baseStyles} ${variants[variant]} ${className}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div className="mt-0.5">
|
||||||
|
<FontAwesomeIcon icon={icon} className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<h3 className="font-medium">{title}</h3>
|
||||||
|
{message && <p className="text-sm opacity-90">{message}</p>}
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2 pt-1">
|
||||||
|
{showRetry && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRetry}
|
||||||
|
disabled={retrying}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-red-600 text-white text-sm hover:bg-red-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotateRight} className="h-4 w-4" />
|
||||||
|
{retrying ? "Retrying…" : "Retry"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm hover:bg-white/50"
|
||||||
|
aria-expanded={expanded}
|
||||||
|
aria-controls="error-details"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={expanded ? faChevronUp : faChevronDown}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
{expanded ? "Hide details" : "Show details"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copyDetails}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md border text-sm hover:bg-white/50"
|
||||||
|
aria-label="Copy error details"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClipboard} className="h-4 w-4" />
|
||||||
|
Copy details
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dev details (collapsible) */}
|
||||||
|
{expanded && details && (
|
||||||
|
<pre
|
||||||
|
id="error-details"
|
||||||
|
className="mt-2 max-h-64 overflow-auto text-xs leading-relaxed bg-white/60 text-red-900 border rounded p-3"
|
||||||
|
>
|
||||||
|
{details}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorState;
|
||||||
14
src/components/UI/Loading.tsx
Normal file
14
src/components/UI/Loading.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
type LoadingProps = {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = ({ message }: LoadingProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-6">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-500 mb-2"></div>
|
||||||
|
{message && <p className="text-lg text-gray-500">{message}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
@@ -17,10 +17,6 @@ export const AlertHitProvider = ({ children }: AlertHitProviderTypeProps) => {
|
|||||||
query?.data?.alertHistory?.forEach((element: SightingType) => {
|
query?.data?.alertHistory?.forEach((element: SightingType) => {
|
||||||
dispatch({ type: "ADD", payload: element });
|
dispatch({ type: "ADD", payload: element });
|
||||||
});
|
});
|
||||||
} else if (query.error) {
|
|
||||||
console.error("Error fetching alert hits:", query.error);
|
|
||||||
} else {
|
|
||||||
console.log("Loading alert hits...");
|
|
||||||
}
|
}
|
||||||
}, [query.data, query.error, query.isLoading]);
|
}, [query.data, query.error, query.isLoading]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { CAM_BASE } from "../utils/config";
|
import { CAM_BASE } from "../utils/config";
|
||||||
import type { CameraBlackBoardOptions } from "../types/types";
|
import type { CameraBlackBoardOptions } from "../types/types";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const getAllBlackboardData = async () => {
|
const getAllBlackboardData = async () => {
|
||||||
const response = await fetch(`${CAM_BASE}/api/blackboard`);
|
const response = await fetch(`${CAM_BASE}/api/blackboard`, {
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch blackboard data");
|
throw new Error("Failed to fetch blackboard data");
|
||||||
}
|
}
|
||||||
@@ -36,7 +40,14 @@ export const useCameraBlackboard = () => {
|
|||||||
path: options?.path,
|
path: options?.path,
|
||||||
value: options?.value,
|
value: options?.value,
|
||||||
}),
|
}),
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(`cannot get data: ${error.message}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.isError) toast.error(query.error.message);
|
||||||
|
}, [query?.error?.message, query.isError]);
|
||||||
|
|
||||||
return { query, mutation };
|
return { query, mutation };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import { CAM_BASE } from "../utils/config";
|
|||||||
|
|
||||||
const base_url = `${CAM_BASE}/api`;
|
const base_url = `${CAM_BASE}/api`;
|
||||||
|
|
||||||
|
const fetch_url = `http://100.82.205.44/api/fetch-config?id=Colour`;
|
||||||
|
console.log(fetch_url);
|
||||||
const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => {
|
const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => {
|
||||||
const [, cameraSide] = queryKey;
|
const [, cameraSide] = queryKey;
|
||||||
const fetchUrl = `${base_url}/fetch-config?id=${cameraSide}`;
|
const fetchUrl = `${base_url}/fetch-config?id=${cameraSide}`;
|
||||||
const response = await fetch(fetchUrl);
|
const response = await fetch(fetchUrl, {
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
});
|
||||||
if (!response.ok) throw new Error("cannot react cameraSide ");
|
if (!response.ok) throw new Error("cannot react cameraSide ");
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
@@ -53,5 +57,6 @@ export const useFetchCameraConfig = (cameraSide: string) => {
|
|||||||
isError: fetchedConfigQuery.isError,
|
isError: fetchedConfigQuery.isError,
|
||||||
updateCameraConfig: updateConfigMutation.mutate,
|
updateCameraConfig: updateConfigMutation.mutate,
|
||||||
updateCameraConfigError: updateConfigMutation.error,
|
updateCameraConfigError: updateConfigMutation.error,
|
||||||
|
updateCameraConfigLoading: updateConfigMutation.isPending,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { CAM_BASE } from "../utils/config";
|
import { CAM_BASE } from "../utils/config";
|
||||||
import type { ModemConfig, WifiConfig } from "../types/types";
|
import type { ModemConfig, WifiConfig } from "../types/types";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const getWiFiSettings = async () => {
|
const getWiFiSettings = async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-wifi`
|
`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-wifi`,
|
||||||
|
{
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Cannot fetch Wifi settings");
|
throw new Error("Cannot fetch Wifi settings");
|
||||||
@@ -29,7 +34,10 @@ const updateWifiSettings = async (wifiConfig: WifiConfig) => {
|
|||||||
|
|
||||||
const getModemSettings = async () => {
|
const getModemSettings = async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-modem`
|
`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-modem`,
|
||||||
|
{
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Cannot fetch modem settings");
|
throw new Error("Cannot fetch modem settings");
|
||||||
@@ -61,7 +69,13 @@ export const useWifiAndModem = () => {
|
|||||||
const wifiMutation = useMutation({
|
const wifiMutation = useMutation({
|
||||||
mutationKey: ["updateWifiSettings"],
|
mutationKey: ["updateWifiSettings"],
|
||||||
mutationFn: (wifiConfig: WifiConfig) => updateWifiSettings(wifiConfig),
|
mutationFn: (wifiConfig: WifiConfig) => updateWifiSettings(wifiConfig),
|
||||||
onError: (error) => console.log(error),
|
onError: (error) => {
|
||||||
|
toast.error("Failed to update WiFi settings");
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("WiFi settings updated successfully");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const modemQuery = useQuery({
|
const modemQuery = useQuery({
|
||||||
@@ -72,8 +86,22 @@ export const useWifiAndModem = () => {
|
|||||||
const modemMutation = useMutation({
|
const modemMutation = useMutation({
|
||||||
mutationKey: ["updateModemSettings"],
|
mutationKey: ["updateModemSettings"],
|
||||||
mutationFn: (modemConfig: ModemConfig) => updateModemSettings(modemConfig),
|
mutationFn: (modemConfig: ModemConfig) => updateModemSettings(modemConfig),
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to update Modem settings");
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Modem settings updated successfully");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wifiQuery.isError) toast.error("Cannot get WiFi settings");
|
||||||
|
}, [wifiQuery?.error?.message, wifiQuery.isError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modemQuery.isError) toast.error("Cannot get Modem settings");
|
||||||
|
}, [modemQuery?.error?.message, modemQuery.isError]);
|
||||||
return {
|
return {
|
||||||
wifiQuery,
|
wifiQuery,
|
||||||
wifiMutation,
|
wifiMutation,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import type { zoomConfig, ZoomInOptions } from "../types/types";
|
|||||||
|
|
||||||
async function zoomIn(options: ZoomInOptions) {
|
async function zoomIn(options: ZoomInOptions) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${CAM_BASE}/Ip${options.camera}-command?magnification=${options.multiplier}x`
|
`${CAM_BASE}/Ip${options.camera}-command?magnification=${options.multiplier}x`,
|
||||||
|
{
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Cannot reach camera zoom endpoint");
|
throw new Error("Cannot reach camera zoom endpoint");
|
||||||
@@ -21,7 +24,9 @@ async function fetchZoomInConfig({
|
|||||||
queryKey,
|
queryKey,
|
||||||
}: QueryFunctionContext<[string, zoomConfig]>) {
|
}: QueryFunctionContext<[string, zoomConfig]>) {
|
||||||
const [, { camera }] = queryKey;
|
const [, { camera }] = queryKey;
|
||||||
const response = await fetch(`${CAM_BASE}/Ip${camera}-inspect`);
|
const response = await fetch(`${CAM_BASE}/Ip${camera}-inspect`, {
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Cannot get camera zoom settings");
|
throw new Error("Cannot get camera zoom settings");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { CAM_BASE } from "../utils/config";
|
import { CAM_BASE } from "../utils/config";
|
||||||
|
|
||||||
const apiUrl = CAM_BASE;
|
const apiUrl = CAM_BASE;
|
||||||
|
// const fetch_url = `http://100.82.205.44/Colour-preview`;
|
||||||
async function fetchSnapshot(cameraSide: string) {
|
async function fetchSnapshot(cameraSide: string) {
|
||||||
const response = await fetch(`${apiUrl}/${cameraSide}-preview`);
|
const response = await fetch(`${apiUrl}/${cameraSide}-preview`, {
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Cannot reach endpoint");
|
throw new Error("Cannot reach endpoint");
|
||||||
}
|
}
|
||||||
@@ -75,9 +77,5 @@ export function useGetOverviewSnapshot(side: string) {
|
|||||||
};
|
};
|
||||||
}, [drawImage]);
|
}, [drawImage]);
|
||||||
|
|
||||||
if (isError) {
|
return { canvasRef, isError, error, isPending };
|
||||||
console.error("Snapshot error:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { canvasRef, isError, isPending };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import type { NPEDFieldType } from "../types/types";
|
|||||||
import { useNPEDContext } from "../context/NPEDUserContext";
|
import { useNPEDContext } from "../context/NPEDUserContext";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CAM_BASE } from "../utils/config";
|
import { CAM_BASE } from "../utils/config";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
async function fetchNPEDDetails() {
|
async function fetchNPEDDetails() {
|
||||||
const fetchUrl = `${CAM_BASE}/api/fetch-config?id=NPED`;
|
const fetchUrl = `${CAM_BASE}/api/fetch-config?id=NPED`;
|
||||||
const response = await fetch(fetchUrl);
|
const response = await fetch(fetchUrl, {
|
||||||
|
signal: AbortSignal.timeout(500),
|
||||||
|
});
|
||||||
if (!response.ok) throw new Error("Cannot reach fetch-config endpoint");
|
if (!response.ok) throw new Error("Cannot reach fetch-config endpoint");
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
@@ -14,43 +17,37 @@ async function fetchNPEDDetails() {
|
|||||||
|
|
||||||
async function signIn(loginDetails: NPEDFieldType) {
|
async function signIn(loginDetails: NPEDFieldType) {
|
||||||
const { frontId, rearId, username, password, clientId } = loginDetails;
|
const { frontId, rearId, username, password, clientId } = loginDetails;
|
||||||
|
|
||||||
const NPEDLoginURLFront = `${CAM_BASE}/api/update-config?id=${frontId}`;
|
const NPEDLoginURLFront = `${CAM_BASE}/api/update-config?id=${frontId}`;
|
||||||
const NPEDLoginURLRear = `${CAM_BASE}/api/update-config?id=${rearId}`;
|
const NPEDLoginURLRear = `${CAM_BASE}/api/update-config?id=${rearId}`;
|
||||||
const frontCameraPayload = {
|
|
||||||
id: frontId,
|
const payload = (id: string) => ({
|
||||||
|
id,
|
||||||
fields: [
|
fields: [
|
||||||
{ property: "propEnabled", value: true },
|
{ property: "propEnabled", value: true },
|
||||||
{ property: "propUsername", value: username },
|
{ property: "propUsername", value: username },
|
||||||
{ property: "propPassword", value: password },
|
{ property: "propPassword", value: password },
|
||||||
{ property: "propClientID", value: clientId },
|
{ property: "propClientID", value: clientId },
|
||||||
],
|
],
|
||||||
};
|
|
||||||
|
|
||||||
const rearCameraPayload = {
|
|
||||||
id: rearId,
|
|
||||||
fields: [
|
|
||||||
{ property: "propEnabled", value: true },
|
|
||||||
{ property: "propUsername", value: username },
|
|
||||||
{ property: "propPassword", value: password },
|
|
||||||
{ property: "propClientID", value: clientId },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const frontCameraResponse = await fetch(NPEDLoginURLFront, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(frontCameraPayload),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rearCameraResponse = await fetch(NPEDLoginURLRear, {
|
const [frontRes, rearRes] = await Promise.all([
|
||||||
|
fetch(NPEDLoginURLFront, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(rearCameraPayload),
|
body: JSON.stringify(payload(frontId)),
|
||||||
});
|
}),
|
||||||
|
fetch(NPEDLoginURLRear, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload(rearId)),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!frontCameraResponse.ok) throw new Error("cannot reach NPED endpoint");
|
if (!frontRes.ok || !rearRes.ok)
|
||||||
if (!rearCameraResponse.ok) throw new Error("cannot reach NPED endpoint");
|
throw new Error("Cannot reach NPED endpoint");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
frontResponse: frontCameraResponse.json(),
|
frontResponse: frontRes.json(),
|
||||||
rearResponse: rearCameraResponse.json(),
|
rearResponse: rearRes.json(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,12 +78,34 @@ export const useNPEDAuth = () => {
|
|||||||
const signInMutation = useMutation({
|
const signInMutation = useMutation({
|
||||||
mutationKey: ["NPEDSignin"],
|
mutationKey: ["NPEDSignin"],
|
||||||
mutationFn: signIn,
|
mutationFn: signIn,
|
||||||
onSuccess: async (data) => setUser(await data.frontResponse),
|
onMutate: () => {
|
||||||
|
toast.loading("Signing in...");
|
||||||
|
},
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
toast.dismiss();
|
||||||
|
toast.success("Signed in successfully!");
|
||||||
|
setUser(await data.frontResponse);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.dismiss();
|
||||||
|
if (error.message.includes("timed out")) {
|
||||||
|
toast.error("Connection timed out. Please check your network.");
|
||||||
|
} else {
|
||||||
|
toast.error(`Sign-in failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const signOutMutation = useMutation({
|
const signOutMutation = useMutation({
|
||||||
mutationKey: ["auth", "NPEDSignOut"],
|
mutationKey: ["auth", "NPEDSignOut"],
|
||||||
mutationFn: signOut,
|
mutationFn: signOut,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Signed out successfully");
|
||||||
|
setUser(null);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(`Sign-out failed: ${error.message}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchdataQuery = useQuery({
|
const fetchdataQuery = useQuery({
|
||||||
@@ -102,6 +121,10 @@ export const useNPEDAuth = () => {
|
|||||||
}
|
}
|
||||||
}, [fetchdataQuery.data, fetchdataQuery.isSuccess, setUser]);
|
}, [fetchdataQuery.data, fetchdataQuery.isSuccess, setUser]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchdataQuery.isError) toast.error(fetchdataQuery.error.message);
|
||||||
|
}, [fetchdataQuery?.error?.message, fetchdataQuery.isError]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signIn: signInMutation.mutate,
|
signIn: signInMutation.mutate,
|
||||||
signInAsync: signInMutation.mutateAsync,
|
signInAsync: signInMutation.mutateAsync,
|
||||||
@@ -109,6 +132,8 @@ export const useNPEDAuth = () => {
|
|||||||
isError: signInMutation.isError,
|
isError: signInMutation.isError,
|
||||||
error: signInMutation.error,
|
error: signInMutation.error,
|
||||||
data: signInMutation.data,
|
data: signInMutation.data,
|
||||||
|
fetchdataQueryError: fetchdataQuery.error,
|
||||||
|
fetchdataQueryLoading: fetchdataQuery.isLoading,
|
||||||
user,
|
user,
|
||||||
setUser,
|
setUser,
|
||||||
signOut: signOutMutation.mutate,
|
signOut: signOutMutation.mutate,
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ async function fetchSighting(
|
|||||||
url: string | undefined,
|
url: string | undefined,
|
||||||
ref: number
|
ref: number
|
||||||
): Promise<SightingType> {
|
): Promise<SightingType> {
|
||||||
const res = await fetch(`${url}${ref}`);
|
const res = await fetch(`${url}${ref}`, {
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
if (!res.ok) throw new Error(String(res.status));
|
if (!res.ok) throw new Error(String(res.status));
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const useSystemConfig = () => {
|
|||||||
const saveSystemSettings = useMutation({
|
const saveSystemSettings = useMutation({
|
||||||
mutationKey: ["systemSaveSettings"],
|
mutationKey: ["systemSaveSettings"],
|
||||||
mutationFn: handleSystemSave,
|
mutationFn: handleSystemSave,
|
||||||
onSuccess: () => toast("System Settings Saved Successfully!"),
|
onError: (error) => console.error(error.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSystemSettings = useQuery({
|
const getSystemSettings = useQuery({
|
||||||
@@ -28,5 +28,8 @@ export const useSystemConfig = () => {
|
|||||||
uploadSettings: uploadSettingsMutation.mutate,
|
uploadSettings: uploadSettingsMutation.mutate,
|
||||||
saveSystemSettings: saveSystemSettings.mutate,
|
saveSystemSettings: saveSystemSettings.mutate,
|
||||||
systemSettingsData: getSystemSettings.data,
|
systemSettingsData: getSystemSettings.data,
|
||||||
|
systemSettingsError: getSystemSettings.error,
|
||||||
|
saveSystemSettingsError: saveSystemSettings.isError,
|
||||||
|
saveSystemSettingsLoading: saveSystemSettings.isPending,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
|
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
|
||||||
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
|
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
|
||||||
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
|
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
|
||||||
import { CAM_BASE } from "../utils/config";
|
import { CAM_BASE, OUTSIDE_CAM_BASE } from "../utils/config";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const mode = import.meta.env.MODE;
|
const mode = import.meta.env.MODE;
|
||||||
const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`;
|
const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`;
|
||||||
|
// const outside_url = `http://100.82.205.44/mergedHistory/sightingSummary?mostRecentRef=`;
|
||||||
console.log(mode);
|
console.log(mode);
|
||||||
|
console.log(OUTSIDE_CAM_BASE);
|
||||||
return (
|
return (
|
||||||
<SightingFeedProvider url={base_url}>
|
<SightingFeedProvider url={base_url}>
|
||||||
<div className="mx-auto flex flex-col lg:flex-row gap-2 px-1 sm:px-2 lg:px-0 w-full min-h-screen">
|
<div className="mx-auto flex flex-col lg:flex-row gap-2 px-1 sm:px-2 lg:px-0 w-full min-h-screen">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Toaster } from "sonner";
|
||||||
import HistoryList from "../components/HistoryList/HistoryList.tsx";
|
import HistoryList from "../components/HistoryList/HistoryList.tsx";
|
||||||
import HitSearchCard from "../components/SessionForm/HitSearchCard.tsx";
|
import HitSearchCard from "../components/SessionForm/HitSearchCard.tsx";
|
||||||
import SessionCard from "../components/SessionForm/SessionCard.tsx";
|
import SessionCard from "../components/SessionForm/SessionCard.tsx";
|
||||||
@@ -15,6 +16,7 @@ const Session = () => {
|
|||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<HistoryList />
|
<HistoryList />
|
||||||
</div>
|
</div>
|
||||||
|
<Toaster />
|
||||||
</div>
|
</div>
|
||||||
</SightingFeedProvider>
|
</SightingFeedProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const randomChars = () => {
|
|||||||
|
|
||||||
export function parseRTSPUrl(url: string) {
|
export function parseRTSPUrl(url: string) {
|
||||||
const regex = /rtsp:\/\/([^:]+):([^@]+)@([^:/]+):?(\d+)?(\/.*)?/;
|
const regex = /rtsp:\/\/([^:]+):([^@]+)@([^:/]+):?(\d+)?(\/.*)?/;
|
||||||
const match = url.match(regex);
|
const match = url?.match(regex);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user