- added prettier config file

- improved sound state to remove pooling

- increased size of naviagtion arrows and fixed navigation onClick

-  decreased width of nav bar

- fixed button to reveal passwords in some password fields
This commit is contained in:
2025-10-15 11:00:52 +01:00
parent 09d5af4035
commit 4da240a204
20 changed files with 211 additions and 224 deletions

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@@ -1,19 +0,0 @@
TODO:
Hotlist upload (Question for Dion about API) and hits popping up in sighting stack.
NPED API working and catagories popping up in sighting stack. Images added to public folder.
Make the friendly name of each camera permeate throughout.
Make favicon MAV logo.
Swipe down to get to session page.
I have made an error I don't know how to fix in SightingFeedProvider.tsx
There is a bug in /front-camera-settings where the navigation arrow doesn't have a transparent background. I don't know why it is only that one and I can't find out why. Very strange.
The selected sighting in the sighting stack seems a tad buggy. Sometimes multiple get selected.
Can the selected sighting be shown in full detail. How this will look is still up for debate. Either as a pop up card as in AiQ Flexi, or in the OVerview card??
How do you know if the time has sync? Make UTC red if not sync.
Can the relative aspect ratio in SightingOverview.tsx be the ratio of image pixel size of the image to best take advantage of the space?
obscure details on dashboard to a toggle
FYI:
Session, WiFi and Modem stuff isn't implimented in the backend. Those are just placeholders for now.

View File

@@ -1,10 +1,5 @@
import { Formik, Field, Form } from "formik"; import { Formik, Field, Form } from "formik";
import type { import type { CameraConfig, CameraSettingErrorValues, CameraSettingValues, ZoomInOptions } from "../../types/types";
CameraConfig,
CameraSettingErrorValues,
CameraSettingValues,
ZoomInOptions,
} from "../../types/types";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
@@ -28,8 +23,7 @@ const CameraSettingFields = ({
updateCameraConfigError, updateCameraConfigError,
}: CameraSettingsProps) => { }: CameraSettingsProps) => {
const [showPwd, setShowPwd] = useState(false); const [showPwd, setShowPwd] = useState(false);
const cameraControllerSide = const cameraControllerSide = initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB";
initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB";
const { mutation, query } = useCameraZoom({ camera: cameraControllerSide }); const { mutation, query } = useCameraZoom({ camera: cameraControllerSide });
const zoomOptions = [1, 2, 4, 8]; const zoomOptions = [1, 2, 4, 8];
@@ -109,9 +103,7 @@ const CameraSettingFields = ({
<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>
{touched.friendlyName && errors.friendlyName && ( {touched.friendlyName && errors.friendlyName && (
<small className="absolute right-0 top-0 text-red-500"> <small className="absolute right-0 top-0 text-red-500">{errors.friendlyName}</small>
{errors.friendlyName}
</small>
)} )}
<Field <Field
id="friendlyName" id="friendlyName"
@@ -126,9 +118,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative"> <div className="flex flex-col space-y-2 relative">
<label htmlFor="cameraAddress">Camera Address</label> <label htmlFor="cameraAddress">Camera Address</label>
{touched.cameraAddress && errors.cameraAddress && ( {touched.cameraAddress && errors.cameraAddress && (
<small className="absolute right-0 top-0 text-red-500"> <small className="absolute right-0 top-0 text-red-500">{errors.cameraAddress}</small>
{errors.cameraAddress}
</small>
)} )}
<Field <Field
id="cameraAddress" id="cameraAddress"
@@ -143,9 +133,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative"> <div className="flex flex-col space-y-2 relative">
<label htmlFor="userName">User Name</label> <label htmlFor="userName">User Name</label>
{touched.userName && errors.userName && ( {touched.userName && errors.userName && (
<small className="absolute right-0 top-0 text-red-500"> <small className="absolute right-0 top-0 text-red-500">{errors.userName}</small>
{errors.userName}
</small>
)} )}
<Field <Field
id="userName" id="userName"
@@ -161,9 +149,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative"> <div className="flex flex-col space-y-2 relative">
<label htmlFor="password">Password</label> <label htmlFor="password">Password</label>
{touched.password && errors.password && ( {touched.password && errors.password && (
<small className="absolute right-0 top-0 text-red-500"> <small className="absolute right-0 top-0 text-red-500">{errors.password}</small>
{errors.password}
</small>
)} )}
<div className="flex gap-2 items-center relative mb-4"> <div className="flex gap-2 items-center relative mb-4">
<Field <Field
@@ -209,10 +195,7 @@ const CameraSettingFields = ({
</div> </div>
<div className="mt-3"> <div className="mt-3">
{updateCameraConfigError ? ( {updateCameraConfigError ? (
<button <button className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full" disabled>
className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full"
disabled
>
Retry Retry
</button> </button>
) : ( ) : (

View File

@@ -15,8 +15,7 @@ const CameraSettings = ({
zoomLevel?: number; zoomLevel?: number;
onZoomLevelChange?: (level: number) => void; onZoomLevelChange?: (level: number) => void;
}) => { }) => {
const { data, updateCameraConfig, updateCameraConfigError } = const { data, updateCameraConfig, updateCameraConfigError } = useFetchCameraConfig(side);
useFetchCameraConfig(side);
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">

View File

@@ -120,6 +120,7 @@ const ChannelFields = () => {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<label htmlFor="password">Password</label> <label htmlFor="password">Password</label>
<div className="flex gap-2 items-center relative mb-4">
<Field <Field
name={"password"} name={"password"}
type={showPwd ? "text" : "password"} type={showPwd ? "text" : "password"}
@@ -137,7 +138,9 @@ const ChannelFields = () => {
onClick={() => setShowPwd((s) => !s)} onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye} icon={showPwd ? faEyeSlash : faEye}
/> />
</div>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<label htmlFor="connectTimeoutSeconds"> <label htmlFor="connectTimeoutSeconds">
Connect Timeout Seconds Connect Timeout Seconds

View File

@@ -73,24 +73,26 @@ const NPEDFields = () => {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<label htmlFor="password">Password</label> <label htmlFor="password">Password</label>
{touched.password && errors.password && ( <div className="flex gap-2 items-center relative mb-4">
<small className="absolute right-0 -top-5 text-red-500">
{errors.password}
</small>
)}
<Field <Field
name="password" name="password"
type={showPwd ? "text" : "password"} type={showPwd ? "text" : "password"}
id="password" id="password"
placeholder="NPED Password" placeholder="NPED Password"
className="p-1.5 border border-gray-400 rounded-lg" className="p-2 border border-gray-400 rounded-lg w-full"
/> />
{touched.password && errors.password && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.password}
</small>
)}
<FontAwesomeIcon <FontAwesomeIcon
type="button" type="button"
className="absolute right-5 end-0" className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)} onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye} icon={showPwd ? faEyeSlash : faEye}
/> />
</div>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<label htmlFor="clientId">Client ID</label> <label htmlFor="clientId">Client ID</label>

View File

@@ -4,9 +4,12 @@ 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 { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const ModemSettings = () => { const ModemSettings = () => {
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showPwd, setShowPwd] = useState(false);
const { modemQuery, modemMutation } = useWifiAndModem(); const { modemQuery, modemMutation } = useWifiAndModem();
const apn = modemQuery?.data?.propAPN?.value; const apn = modemQuery?.data?.propAPN?.value;
@@ -102,13 +105,21 @@ const ModemSettings = () => {
> >
Password Password
</label> </label>
<div className="flex gap-2 items-center relative mb-4">
<Field <Field
placeholder="Enter Password"
name="password"
id="password" id="password"
type="text" name="password"
className="p-1.5 border border-gray-400 rounded-lg" type={showPwd ? "text" : "password"}
className="p-2 border border-gray-400 rounded-lg w-full"
placeholder="Enter Password"
/> />
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
</div>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<label <label

View File

@@ -66,11 +66,12 @@ const WiFiSettingsForm = () => {
> >
Password Password
</label> </label>
<div className="flex gap-2 items-center relative mb-4">
<Field <Field
id="password" id="password"
name="password" name="password"
type={showPwd ? "text" : "password"} type={showPwd ? "text" : "password"}
className="p-1.5 border border-gray-400 rounded-lg" className="p-2 border border-gray-400 rounded-lg w-full"
placeholder="Enter Password" placeholder="Enter Password"
/> />
<FontAwesomeIcon <FontAwesomeIcon
@@ -79,6 +80,7 @@ const WiFiSettingsForm = () => {
onClick={() => setShowPwd((s) => !s)} onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye} icon={showPwd ? faEyeSlash : faEye}
/> />
</div>
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<label <label

View File

@@ -6,7 +6,7 @@ type FormGroupProps = {
const FormGroup = ({ children }: FormGroupProps) => { const FormGroup = ({ children }: FormGroupProps) => {
return ( return (
<div className="flex flex-col md:flex-row items-center justify-between relative"> <div className="flex flex-col md:flex-row md:items-center justify-between relative">
{children} {children}
</div> </div>
); );

View File

@@ -32,8 +32,8 @@ export default function Header() {
}; };
return ( return (
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4 flex flex-col md:flex-row justify-between mb-7 space-y-6 md:space-y-0"> <div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto sm:px-3 lg:px-4 py-4 flex flex-col md:flex-row justify-between mb-7 space-y-6 md:space-y-0">
<div className="w-30"> <div className="w-28">
<Link to={"/"}> <Link to={"/"}>
<img src={Logo} alt="Logo" width={150} height={150} /> <img src={Logo} alt="Logo" width={150} height={150} />
</Link> </Link>

View File

@@ -11,13 +11,14 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const navigationDest = (side: string | undefined) => { const navigationDest = (side: string | undefined) => {
console.log(side);
if (settingsPage) { if (settingsPage) {
navigate("/"); navigate("/");
return; return;
} }
if (side === "Front") { if (side === "Front") {
navigate("/front-camera-settings"); navigate("/camera-settings");
} else if (side === "Rear") { } else if (side === "Rear") {
navigate("/Rear-Camera-settings"); navigate("/Rear-Camera-settings");
} }
@@ -28,13 +29,15 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
<> <>
{side === "CameraA" ? ( {side === "CameraA" ? (
<FontAwesomeIcon <FontAwesomeIcon
size="2xl"
icon={faArrowRight} icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-lg hover:cursor-pointer animate-bounce z-30" className="absolute top-[50%] right-[2%] backdrop-blur-lg hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)} onClick={() => navigationDest("Front")}
/> />
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowLeft} icon={faArrowLeft}
size="2xl"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30" className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)} onClick={() => navigationDest(side)}
/> />
@@ -46,14 +49,16 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
<> <>
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowLeft} icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30" size="2xl"
onClick={() => navigationDest(side)} className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-100 "
onClick={() => navigationDest("Front")}
/> />
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowRight} icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30" size="2xl"
onClick={() => navigationDest(side)} className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-100"
onClick={() => navigationDest("Rear")}
/> />
</> </>
); );

View File

@@ -4,6 +4,7 @@ import type { SoundAction, SoundState } from "../types/types";
type SoundContextType = { type SoundContextType = {
state: SoundState; state: SoundState;
dispatch: Dispatch<SoundAction>; dispatch: Dispatch<SoundAction>;
audioArmed: boolean;
}; };
export const SoundContext = createContext<SoundContextType | undefined>( export const SoundContext = createContext<SoundContextType | undefined>(

View File

@@ -1,4 +1,11 @@
import { useEffect, useMemo, useReducer, type ReactNode } from "react"; import {
useEffect,
useMemo,
useReducer,
useRef,
useState,
type ReactNode,
} from "react";
import { SoundContext } from "../SoundContext"; import { SoundContext } from "../SoundContext";
import { initialState, reducer } from "../reducers/SoundContextReducer"; import { initialState, reducer } from "../reducers/SoundContextReducer";
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard"; import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
@@ -8,6 +15,9 @@ type SoundContextProviderProps = {
}; };
const SoundContextProvider = ({ children }: SoundContextProviderProps) => { const SoundContextProvider = ({ children }: SoundContextProviderProps) => {
const audioReady = useRef(false);
const [audioArmed, setAudioArmed] = useState(false);
const audioCtxRef = useRef<AudioContext | null>(null);
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const { mutation } = useCameraBlackboard(); const { mutation } = useCameraBlackboard();
@@ -23,7 +33,40 @@ const SoundContextProvider = ({ children }: SoundContextProviderProps) => {
fetchSound(); fetchSound();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);
useEffect(() => {
const unlock = async () => {
if (!audioCtxRef.current) audioCtxRef.current = new AudioContext();
if (audioCtxRef.current.state !== "running") {
try {
await audioCtxRef.current.resume();
} catch {
/* empty */
}
}
const armed = audioCtxRef.current.state === "running";
audioReady.current = audioCtxRef.current.state === "running";
setAudioArmed(armed);
if (audioReady.current) {
window.removeEventListener("pointerdown", unlock);
window.removeEventListener("keydown", unlock);
window.removeEventListener("touchstart", unlock);
}
};
window.addEventListener("pointerdown", unlock, { once: false });
window.addEventListener("keydown", unlock, { once: false });
window.addEventListener("touchstart", unlock, { once: false });
return () => {
window.removeEventListener("pointerdown", unlock);
window.removeEventListener("keydown", unlock);
window.removeEventListener("touchstart", unlock);
};
}, []);
const value = useMemo(
() => ({ state, dispatch, audioArmed }),
[state, audioArmed]
);
return ( return (
<SoundContext.Provider value={value}>{children}</SoundContext.Provider> <SoundContext.Provider value={value}>{children}</SoundContext.Provider>
); );

View File

@@ -50,8 +50,7 @@ export const useCameraBlackboard = () => {
}); });
useEffect(() => { useEffect(() => {
if (query.isError) if (query.isError) toast.error(query.error.message, { id: "viewBlackboardData" });
toast.error(query.error.message, { id: "viewBlackboardData" });
}, [query?.error?.message, query.isError]); }, [query?.error?.message, query.isError]);
return { query, mutation }; return { query, mutation };

View File

@@ -14,11 +14,7 @@ const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => {
return response.json(); return response.json();
}; };
const updateCamerasideConfig = async (data: { const updateCamerasideConfig = async (data: { id: string | number; friendlyName: string; cameraAddress: string }) => {
id: string | number;
friendlyName: string;
cameraAddress: string;
}) => {
const updateUrl = `${base_url}/update-config?id=${data.id}`; const updateUrl = `${base_url}/update-config?id=${data.id}`;
const updateConfigPayload = { const updateConfigPayload = {
@@ -30,7 +26,7 @@ const updateCamerasideConfig = async (data: {
}, },
], ],
}; };
console.log(updateConfigPayload);
const response = await fetch(updateUrl, { const response = await fetch(updateUrl, {
method: "POST", method: "POST",
body: JSON.stringify(updateConfigPayload), body: JSON.stringify(updateConfigPayload),

View File

@@ -34,9 +34,7 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => {
}; };
const getBackOfficeConfig = async () => { const getBackOfficeConfig = async () => {
const response = await fetch( const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher-json`);
`${CAM_BASE}/api/fetch-config?id=Dispatcher-json`
);
if (!response.ok) throw new Error("Cannot get Back Office configuration"); if (!response.ok) throw new Error("Cannot get Back Office configuration");
return response.json(); return response.json();
}; };
@@ -67,13 +65,10 @@ const updateBackOfficeConfig = async (data: InitialValuesForm) => {
}, },
], ],
}; };
const response = await fetch( const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher-json`, {
`${CAM_BASE}/api/update-config?id=Dispatcher-json`,
{
method: "POST", method: "POST",
body: JSON.stringify(updateConfigPayload), body: JSON.stringify(updateConfigPayload),
} });
);
if (!response.ok) throw new Error("Cannot update Back Office configuration"); if (!response.ok) throw new Error("Cannot update Back Office configuration");
return response.json(); return response.json();
}; };

View File

@@ -5,12 +5,9 @@ import { useEffect } from "react";
import { toast } from "sonner"; 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), signal: AbortSignal.timeout(500),
} });
);
if (!response.ok) { if (!response.ok) {
throw new Error("Cannot fetch Wifi settings"); throw new Error("Cannot fetch Wifi settings");
} }
@@ -18,14 +15,11 @@ const getWiFiSettings = async () => {
}; };
const updateWifiSettings = async (wifiConfig: WifiConfig) => { const updateWifiSettings = async (wifiConfig: WifiConfig) => {
const response = await fetch( const response = await fetch(`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-wifi`, {
`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-wifi`,
{
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(wifiConfig), body: JSON.stringify(wifiConfig),
} });
);
if (!response.ok) { if (!response.ok) {
throw new Error("Cannot update wifi settings"); throw new Error("Cannot update wifi settings");
} }
@@ -33,12 +27,9 @@ 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), signal: AbortSignal.timeout(500),
} });
);
if (!response.ok) { if (!response.ok) {
throw new Error("Cannot fetch modem settings"); throw new Error("Cannot fetch modem settings");
} }
@@ -46,14 +37,11 @@ const getModemSettings = async () => {
}; };
const updateModemSettings = async (modemConfig: ModemConfig) => { const updateModemSettings = async (modemConfig: ModemConfig) => {
const response = await fetch( const response = await fetch(`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-modem`, {
`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-modem`,
{
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(modemConfig), body: JSON.stringify(modemConfig),
} });
);
if (!response.ok) { if (!response.ok) {
throw new Error("cannot update modem settings"); throw new Error("cannot update modem settings");
} }
@@ -100,13 +88,11 @@ export const useWifiAndModem = () => {
}); });
useEffect(() => { useEffect(() => {
if (wifiQuery.isError) if (wifiQuery.isError) toast.error("Cannot get WiFi settings", { id: "wiFiSettings" });
toast.error("Cannot get WiFi settings", { id: "wiFiSettings" });
}, [wifiQuery?.error?.message, wifiQuery.isError]); }, [wifiQuery?.error?.message, wifiQuery.isError]);
useEffect(() => { useEffect(() => {
if (modemQuery.isError) if (modemQuery.isError) toast.error("Cannot get Modem settings", { id: "modemSettings" });
toast.error("Cannot get Modem settings", { id: "modemSettings" });
}, [modemQuery?.error?.message, modemQuery.isError]); }, [modemQuery?.error?.message, modemQuery.isError]);
return { return {
wifiQuery, wifiQuery,

View File

@@ -1,15 +1,12 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { Query, useQuery } from "@tanstack/react-query";
import type { SightingType } from "../types/types"; import type { SightingType } from "../types/types";
import { useSoundOnChange } from "react-sounds"; import { useSoundOnChange } from "react-sounds";
import { useSoundContext } from "../context/SoundContext"; import { useSoundContext } from "../context/SoundContext";
import { getSoundFileURL } from "../utils/utils"; import { getSoundFileURL } from "../utils/utils";
import switchSound from "../assets/sounds/ui/switch.mp3"; import switchSound from "../assets/sounds/ui/switch.mp3";
async function fetchSighting( async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
url: string | undefined,
ref: number
): Promise<SightingType> {
const res = await fetch(`${url}${ref}`, { const res = await fetch(`${url}${ref}`, {
signal: AbortSignal.timeout(5000), signal: AbortSignal.timeout(5000),
}); });
@@ -18,44 +15,40 @@ async function fetchSighting(
} }
export function useSightingFeed(url: string | undefined) { export function useSightingFeed(url: string | undefined) {
const { state } = useSoundContext(); const { state, audioArmed } = useSoundContext();
const [sightings, setSightings] = useState<SightingType[]>([]); const [sightings, setSightings] = useState<SightingType[]>([]);
const [selectedRef, setSelectedRef] = useState<number | null>(null); const [selectedRef, setSelectedRef] = useState<number | null>(null);
const [sessionStarted, setSessionStarted] = useState(false); const [sessionStarted, setSessionStarted] = useState(false);
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
const mostRecent = sightings[0] ?? null; const mostRecent = sightings[0] ?? null;
const latestRef = mostRecent?.ref ?? null; const latestRef = mostRecent?.ref ?? null;
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(
null
);
const first = useRef(true); const first = useRef(true);
const lastSoundAt = useRef(0);
const COOLDOWN_MS = 1500;
const currentRef = useRef<number>(-1);
const lastValidTimestamp = useRef<number>(Date.now());
const trigger = useMemo(() => { const trigger = useMemo(() => {
if (latestRef == null) return null; if (latestRef == null || !audioArmed) return null;
if (first.current) { if (first.current) {
first.current = false; first.current = false;
return Symbol("skip"); return Symbol("skip");
} }
const now = Date.now();
if (now - lastSoundAt.current < COOLDOWN_MS) return Symbol("skip");
lastSoundAt.current = now;
return latestRef; return latestRef;
}, [latestRef]); }, [audioArmed, latestRef]);
const soundSrc = useMemo(() => { const soundSrc = useMemo(() => {
return getSoundFileURL(state?.sightingSound) ?? switchSound; return getSoundFileURL(state?.sightingSound) ?? switchSound;
}, [state.sightingSound]); }, [state.sightingSound]);
//use latestref instead of trigger to revert back function refetchInterval(query: Query<SightingType, Error, SightingType, (string | undefined)[]>) {
useSoundOnChange(soundSrc, trigger, { if (!query) return;
volume: 1, const data = query.state.data as SightingType | undefined;
});
const currentRef = useRef<number>(-1);
const lastValidTimestamp = useRef<number>(Date.now());
const query = useQuery({
queryKey: ["sighting-feed", url],
enabled: !!url,
queryFn: () => fetchSighting(url, currentRef.current),
refetchInterval: (q) => {
const data = q.state.data as SightingType | undefined;
const now = Date.now(); const now = Date.now();
if (data && data.ref !== -1) { if (data && data.ref !== -1) {
@@ -68,36 +61,31 @@ export function useSightingFeed(url: string | undefined) {
lastValidTimestamp.current = now; lastValidTimestamp.current = now;
} }
return 400; return 400;
}, }
const query = useQuery({
queryKey: ["sighting-feed", url],
enabled: !!url,
queryFn: () => fetchSighting(url, currentRef.current),
refetchInterval: (q) => refetchInterval(q),
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: false, retry: false,
staleTime: 0, staleTime: 0,
}); });
//use latestref instead of trigger to revert back
useSoundOnChange(soundSrc, trigger, {
volume: 1,
initial: false,
});
useEffect(() => { useEffect(() => {
const data = query.data; const data = query.data;
if (!data) return; if (!data || data.ref === -1) return;
const now = Date.now(); const now = Date.now();
if (data.ref === -1) {
// setSightings((prev) => {
// if (prev[0]?.ref === data.ref) return prev;
// const dedupPrev = prev.filter((s) => s.ref !== data.ref);
// return [data, ...dedupPrev].slice(0, 7);
// });
return;
}
// if (Notification.permission === "granted") {
// new Notification("New Sighting!", {
// body: `Ref: ${data.ref}`,
// icon: "/MAV-blue.svg",
// });
// }
currentRef.current = data.ref; currentRef.current = data.ref;
lastValidTimestamp.current = now; lastValidTimestamp.current = now;
@@ -110,11 +98,6 @@ export function useSightingFeed(url: string | undefined) {
setSelectedRef(data.ref); setSelectedRef(data.ref);
}, [query.data]); }, [query.data]);
useEffect(() => {
if (query.error) {
// console.error("Sighting feed error:", query.error);
}
}, [query.error]);
return { return {
sightings, sightings,
selectedRef, selectedRef,

View File

@@ -5,7 +5,7 @@ import { CAM_BASE } from "../utils/config";
const Dashboard = () => { const Dashboard = () => {
const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`; const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`;
// const folkstone = `http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=`;
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

@@ -14,12 +14,7 @@ const FrontCamera = () => {
zoomLevel={zoomLevel} zoomLevel={zoomLevel}
onZoomLevelChange={setZoomLevel} onZoomLevelChange={setZoomLevel}
/> />
<CameraSettings <CameraSettings title="Camera A Settings" side="CameraA" zoomLevel={zoomLevel} onZoomLevelChange={setZoomLevel} />
title="Camera A Settings"
side="CameraA"
zoomLevel={zoomLevel}
onZoomLevelChange={setZoomLevel}
/>
<Toaster /> <Toaster />
</div> </div>
); );