Merged in enhancement/loading+errorstates (pull request #8)

Enhancement/loading+errorstates
This commit is contained in:
2025-10-06 14:21:29 +00:00
27 changed files with 493 additions and 257 deletions

View File

@@ -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}

View File

@@ -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>
<button <div className="mt-3">
type="submit" {updateCameraConfigError ? (
className="bg-[#26B170] text-white rounded-lg p-2 mx-auto h-[100%] w-full" <button className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full">
> Retry
Save settings </button>
</button> ) : (
<button
type="submit"
className="bg-[#26B170] text-white rounded-lg p-2 mx-auto h-[100%] w-full"
>
{isSubmitting ? "Saving" : "Save settings"}
</button>
)}
</div>
</div> </div>
</Form> </Form>
)} )}

View File

@@ -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>
); );

View File

@@ -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);
}; };

View File

@@ -27,12 +27,11 @@ const NPEDFields = () => {
rearId: "NPED", rearId: "NPED",
}; };
const handleSubmit = (values: NPEDFieldType) => { const handleSubmit = async (values: NPEDFieldType) => {
const valuesToSend = { const valuesToSend = {
...values, ...values,
}; };
signIn(valuesToSend); await signIn(valuesToSend);
toast.success("Signed into NPED Successfully");
}; };
const validateValues = (values: NPEDFieldType) => { const validateValues = (values: NPEDFieldType) => {
@@ -55,7 +54,7 @@ const NPEDFields = () => {
validate={validateValues} validate={validateValues}
enableReinitialize enableReinitialize
> >
{({ errors, touched }) => ( {({ 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 htmlFor="username">Username</label> <label htmlFor="username">Username</label>
@@ -113,7 +112,7 @@ const NPEDFields = () => {
type="submit" type="submit"
className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5 hover:cursor-pointer" className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5 hover:cursor-pointer"
> >
Login {isSubmitting ? "Logging in..." : "Login"}
</button> </button>
) : ( ) : (
<button <button

View File

@@ -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) {
console.error(err); if (err instanceof Error) {
toast.error(`Failed to save system settings: ${err.message}`);
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);

View File

@@ -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"}

View File

@@ -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);
@@ -26,7 +25,7 @@ const ModemSettings = () => {
authenticationType: "PAP", authenticationType: "PAP",
}; };
const handleSubmit = (values: ModemSettingsType) => { const handleSubmit = async (values: ModemSettingsType) => {
const modemConfig = { const modemConfig = {
id: "ModemAndWifiManager-modem", id: "ModemAndWifiManager-modem",
fields: [ fields: [
@@ -49,12 +48,7 @@ const ModemSettings = () => {
}, },
], ],
}; };
modemMutation.mutate(modemConfig); await modemMutation.mutateAsync(modemConfig);
if (modemMutation.error) {
toast.error("Failed to update modem settings");
return;
}
toast.success("Modem settings updated");
}; };
return ( return (
@@ -69,76 +63,80 @@ const ModemSettings = () => {
onSubmit={handleSubmit} onSubmit={handleSubmit}
enableReinitialize enableReinitialize
> >
<Form className="flex flex-col space-y-5 px-2"> {({ isSubmitting }) => (
<FormGroup> <Form className="flex flex-col space-y-5 px-2">
<label <FormGroup>
htmlFor="apn" <label
className="font-medium whitespace-nowrap md:w-2/3" htmlFor="apn"
className="font-medium whitespace-nowrap md:w-2/3"
>
APN
</label>
<Field
placeholder="Enter APN"
name="apn"
id="apn"
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="username"
className="font-medium whitespace-nowrap md:w-2/3"
>
Username
</label>
<Field
placeholder="Enter Username"
name="username"
id="username"
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="password"
className="font-medium whitespace-nowrap md:w-2/3"
>
Password
</label>
<Field
placeholder="Enter Password"
name="password"
id="password"
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="password"
className="font-medium whitespace-nowrap md:w-2/3"
>
Password
</label>
<Field
name="authenticationType"
as="select"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 w-2/3"
>
<option value="PAP">PAP</option>
<option value="CHAP">CHAP</option>
<option value="none">None</option>
</Field>
</FormGroup>
<button
type="submit"
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full md:w-[50%]"
> >
APN {isSubmitting || modemMutation.isPending
</label> ? "Saving..."
<Field : "Save Modem settings"}
placeholder="Enter APN" </button>
name="apn" </Form>
id="apn" )}
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="username"
className="font-medium whitespace-nowrap md:w-2/3"
>
Username
</label>
<Field
placeholder="Enter Username"
name="username"
id="username"
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="password"
className="font-medium whitespace-nowrap md:w-2/3"
>
Password
</label>
<Field
placeholder="Enter Password"
name="password"
id="password"
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
</FormGroup>
<FormGroup>
<label
htmlFor="password"
className="font-medium whitespace-nowrap md:w-2/3"
>
Password
</label>
<Field
name="authenticationType"
as="select"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 w-2/3"
>
<option value="PAP">PAP</option>
<option value="CHAP">CHAP</option>
<option value="none">None</option>
</Field>
</FormGroup>
<button
type="submit"
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full md:w-[50%]"
>
Save Modem settings
</button>
</Form>
</Formik> </Formik>
)} )}
</> </>

View File

@@ -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";
@@ -20,7 +19,7 @@ const WiFiSettingsForm = () => {
encryption: "WPA2", encryption: "WPA2",
}; };
const handleSubmit = (values: WifiSettingValues) => { const handleSubmit = async (values: WifiSettingValues) => {
const wifiConfig = { const wifiConfig = {
id: "ModemAndWifiManager-wifi", id: "ModemAndWifiManager-wifi",
fields: [ fields: [
@@ -35,13 +34,7 @@ const WiFiSettingsForm = () => {
], ],
}; };
wifiMutation.mutate(wifiConfig); await wifiMutation.mutateAsync(wifiConfig);
if (wifiMutation.error) {
toast.error("Failed to update WiFi settings");
return;
}
toast.success("WiFi settings updated");
}; };
return ( return (
<Formik <Formik
@@ -49,7 +42,7 @@ const WiFiSettingsForm = () => {
onSubmit={handleSubmit} onSubmit={handleSubmit}
enableReinitialize enableReinitialize
> >
{() => ( {({ 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
@@ -110,7 +103,9 @@ const WiFiSettingsForm = () => {
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%]"
> >
Save WiFi settings {isSubmitting || wifiMutation.isPending
? "Saving..."
: " Save WiFi settings"}
</button> </button>
</Form> </Form>
)} )}

View File

@@ -1,15 +0,0 @@
import { useLatestSighting } from "../../hooks/useLatestSighting";
const SightingCanvas = () => {
const { canvasRef } = useLatestSighting();
return (
<div className="w-70 flex flex-col">
<canvas
ref={canvasRef}
className="items-center w-full h-10 object-contain block"
/>
</div>
);
};
export default SightingCanvas;

View File

@@ -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} />

View File

@@ -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) => {

View 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;

View 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;

View File

@@ -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]);

View File

@@ -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,17 @@ export const useCameraBlackboard = () => {
path: options?.path, path: options?.path,
value: options?.value, value: options?.value,
}), }),
onError: (error) => {
toast.error(`cannot get data: ${error.message}`, {
id: "viewBlackboardData",
});
},
}); });
useEffect(() => {
if (query.isError)
toast.error(query.error.message, { id: "viewBlackboardData" });
}, [query?.error?.message, query.isError]);
return { query, mutation }; return { query, mutation };
}; };

View File

@@ -7,7 +7,9 @@ const base_url = `${CAM_BASE}/api`;
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 +55,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,
}; };
}; };

View File

@@ -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,15 @@ 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", { id: "wiFiSettings" });
console.error(error);
},
onSuccess: () => {
toast.success("WiFi settings updated successfully", {
id: "wiFiSettings",
});
},
}); });
const modemQuery = useQuery({ const modemQuery = useQuery({
@@ -72,8 +88,26 @@ 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", { id: "modemSettings" });
console.error(error);
},
onSuccess: () => {
toast.success("Modem settings updated successfully", {
id: "modemSettings",
});
},
}); });
useEffect(() => {
if (wifiQuery.isError)
toast.error("Cannot get WiFi settings", { id: "wiFiSettings" });
}, [wifiQuery?.error?.message, wifiQuery.isError]);
useEffect(() => {
if (modemQuery.isError)
toast.error("Cannot get Modem settings", { id: "modemSettings" });
}, [modemQuery?.error?.message, modemQuery.isError]);
return { return {
wifiQuery, wifiQuery,
wifiMutation, wifiMutation,

View File

@@ -5,10 +5,14 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { CAM_BASE } from "../utils/config"; import { CAM_BASE } from "../utils/config";
import type { zoomConfig, ZoomInOptions } from "../types/types"; import type { zoomConfig, ZoomInOptions } from "../types/types";
import { toast } from "sonner";
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 +25,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");
} }
@@ -32,6 +38,9 @@ export const useCameraZoom = (options: zoomConfig) => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ["zoomIn"], mutationKey: ["zoomIn"],
mutationFn: (options: ZoomInOptions) => zoomIn(options), mutationFn: (options: ZoomInOptions) => zoomIn(options),
onError: () => {
toast.error("Failed to update zoom settings", { id: "zoom" });
},
}); });
const query = useQuery({ const query = useQuery({

View File

@@ -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 };
} }

View File

@@ -1,60 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useCallback, useEffect, useRef } from "react";
async function fetchSighting() {
const response = await fetch(
// `http://100.82.205.44/api`
`http://192.168.75.11/mergedHistory/sightingSummary?mostRecentRef=-1`
);
if (!response.ok) throw new Error("Failed to fetch sighting");
return response.json();
}
export function useLatestSighting() {
const latestUrlRef = useRef<string | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const sightingImageRef = useRef<HTMLImageElement | null>(null);
const drawImage = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
const img2 = sightingImageRef.current;
if (!img2 || !canvas) return;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
ctx?.drawImage(img2, 0, 0, 200, 50);
}, [sightingImageRef]);
const { data } = useQuery({
queryKey: ["latestSighting"],
queryFn: fetchSighting,
refetchInterval: 100,
});
useEffect(() => {
const img = new Image();
sightingImageRef.current = img;
img.onload = () => {
drawImage();
};
img.src = data?.plateUrlColour;
if (latestUrlRef.current) {
URL.revokeObjectURL(latestUrlRef.current);
}
latestUrlRef.current = img.src;
return () => {
if (latestUrlRef.current) {
URL.revokeObjectURL(latestUrlRef.current);
latestUrlRef.current = null;
}
};
}, [data?.plateUrlColour, drawImage]);
return { data, sightingImageRef, canvasRef };
}

View File

@@ -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([
method: "POST", fetch(NPEDLoginURLFront, {
body: JSON.stringify(rearCameraPayload), method: "POST",
}); 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,

View File

@@ -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();
} }

View File

@@ -5,28 +5,49 @@ import {
handleSystemSave, handleSystemSave,
handleSystemRecall, handleSystemRecall,
} from "../components/SettingForms/System/SettingSaveRecall"; } from "../components/SettingForms/System/SettingSaveRecall";
import { useEffect } from "react";
export const useSystemConfig = () => { export const useSystemConfig = () => {
const uploadSettingsMutation = useMutation({ const uploadSettingsMutation = useMutation({
mutationKey: ["uploadSettings"], mutationKey: ["uploadSettings"],
mutationFn: sendBlobFileUpload, mutationFn: sendBlobFileUpload,
onError: (error) => toast.error(error.message), onError: (error) =>
onSuccess: (test) => toast(test), toast.error(error.message, {
id: "uploadSettings",
}),
onSuccess: (test) =>
toast(test, {
id: "uploadSettings",
}),
}); });
const saveSystemSettings = useMutation({ const saveSystemSettings = useMutation({
mutationKey: ["systemSaveSettings"], mutationKey: ["systemSaveSettings"],
mutationFn: handleSystemSave, mutationFn: handleSystemSave,
onSuccess: () => toast("System Settings Saved Successfully!"), onError: (error) =>
toast.error(error.message, {
id: "systemSettings",
}),
}); });
const getSystemSettings = useQuery({ const getSystemSettings = useQuery({
queryKey: ["getSystemSettings"], queryKey: ["getSystemSettings"],
queryFn: handleSystemRecall, queryFn: handleSystemRecall,
}); });
useEffect(() => {
if (getSystemSettings.isError)
toast.error(getSystemSettings.error.message, {
id: "systemSettings",
});
}, [getSystemSettings?.error?.message, getSystemSettings.isError]);
return { return {
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,
}; };
}; };

View File

@@ -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">

View File

@@ -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>
); );

View File

@@ -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;