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

View File

@@ -15,8 +15,7 @@ const CameraSettings = ({
zoomLevel?: number;
onZoomLevelChange?: (level: number) => void;
}) => {
const { data, updateCameraConfig, updateCameraConfigError } =
useFetchCameraConfig(side);
const { data, updateCameraConfig, updateCameraConfigError } = useFetchCameraConfig(side);
return (
<Card className="overflow-hidden min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[40%] p-4">

View File

@@ -120,24 +120,27 @@ const ChannelFields = () => {
</FormGroup>
<FormGroup>
<label htmlFor="password">Password</label>
<Field
name={"password"}
type={showPwd ? "text" : "password"}
id="password"
placeholder="Back office password"
className={`p-1.5 border ${
errors.password && touched.password
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
<div className="flex gap-2 items-center relative mb-4">
<Field
name={"password"}
type={showPwd ? "text" : "password"}
id="password"
placeholder="Back office password"
className={`p-1.5 border ${
errors.password && touched.password
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
</div>
</FormGroup>
<FormGroup>
<label htmlFor="connectTimeoutSeconds">
Connect Timeout Seconds

View File

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

View File

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

View File

@@ -66,19 +66,21 @@ const WiFiSettingsForm = () => {
>
Password
</label>
<Field
id="password"
name="password"
type={showPwd ? "text" : "password"}
className="p-1.5 border border-gray-400 rounded-lg"
placeholder="Enter Password"
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
<div className="flex gap-2 items-center relative mb-4">
<Field
id="password"
name="password"
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>
<label

View File

@@ -6,7 +6,7 @@ type FormGroupProps = {
const FormGroup = ({ children }: FormGroupProps) => {
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}
</div>
);

View File

@@ -32,8 +32,8 @@ export default function Header() {
};
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="w-30">
<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-28">
<Link to={"/"}>
<img src={Logo} alt="Logo" width={150} height={150} />
</Link>

View File

@@ -11,13 +11,14 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
const navigate = useNavigate();
const navigationDest = (side: string | undefined) => {
console.log(side);
if (settingsPage) {
navigate("/");
return;
}
if (side === "Front") {
navigate("/front-camera-settings");
navigate("/camera-settings");
} else if (side === "Rear") {
navigate("/Rear-Camera-settings");
}
@@ -28,13 +29,15 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
<>
{side === "CameraA" ? (
<FontAwesomeIcon
size="2xl"
icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-lg hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
onClick={() => navigationDest("Front")}
/>
) : (
<FontAwesomeIcon
icon={faArrowLeft}
size="2xl"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
/>
@@ -46,14 +49,16 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
<>
<FontAwesomeIcon
icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
size="2xl"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-100 "
onClick={() => navigationDest("Front")}
/>
<FontAwesomeIcon
icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
size="2xl"
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 = {
state: SoundState;
dispatch: Dispatch<SoundAction>;
audioArmed: boolean;
};
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 { initialState, reducer } from "../reducers/SoundContextReducer";
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
@@ -8,6 +15,9 @@ type 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 { mutation } = useCameraBlackboard();
@@ -23,7 +33,40 @@ const SoundContextProvider = ({ children }: SoundContextProviderProps) => {
fetchSound();
// 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 (
<SoundContext.Provider value={value}>{children}</SoundContext.Provider>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,12 @@
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 { useSoundOnChange } from "react-sounds";
import { useSoundContext } from "../context/SoundContext";
import { getSoundFileURL } from "../utils/utils";
import switchSound from "../assets/sounds/ui/switch.mp3";
async function fetchSighting(
url: string | undefined,
ref: number
): Promise<SightingType> {
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
const res = await fetch(`${url}${ref}`, {
signal: AbortSignal.timeout(5000),
});
@@ -18,86 +15,77 @@ async function fetchSighting(
}
export function useSightingFeed(url: string | undefined) {
const { state } = useSoundContext();
const { state, audioArmed } = useSoundContext();
const [sightings, setSightings] = useState<SightingType[]>([]);
const [selectedRef, setSelectedRef] = useState<number | null>(null);
const [sessionStarted, setSessionStarted] = useState(false);
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
const mostRecent = sightings[0] ?? null;
const latestRef = mostRecent?.ref ?? null;
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(
null
);
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(() => {
if (latestRef == null) return null;
if (latestRef == null || !audioArmed) return null;
if (first.current) {
first.current = false;
return Symbol("skip");
}
const now = Date.now();
if (now - lastSoundAt.current < COOLDOWN_MS) return Symbol("skip");
lastSoundAt.current = now;
return latestRef;
}, [latestRef]);
}, [audioArmed, latestRef]);
const soundSrc = useMemo(() => {
return getSoundFileURL(state?.sightingSound) ?? switchSound;
}, [state.sightingSound]);
//use latestref instead of trigger to revert back
useSoundOnChange(soundSrc, trigger, {
volume: 1,
});
function refetchInterval(query: Query<SightingType, Error, SightingType, (string | undefined)[]>) {
if (!query) return;
const data = query.state.data as SightingType | undefined;
const now = Date.now();
const currentRef = useRef<number>(-1);
const lastValidTimestamp = useRef<number>(Date.now());
if (data && data.ref !== -1) {
lastValidTimestamp.current = now;
return 100;
}
if (now - lastValidTimestamp.current > 60_000) {
currentRef.current = -1;
lastValidTimestamp.current = now;
}
return 400;
}
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();
if (data && data.ref !== -1) {
lastValidTimestamp.current = now;
return 100;
}
if (now - lastValidTimestamp.current > 60_000) {
currentRef.current = -1;
lastValidTimestamp.current = now;
}
return 400;
},
refetchInterval: (q) => refetchInterval(q),
refetchIntervalInBackground: true,
refetchOnWindowFocus: false,
retry: false,
staleTime: 0,
});
//use latestref instead of trigger to revert back
useSoundOnChange(soundSrc, trigger, {
volume: 1,
initial: false,
});
useEffect(() => {
const data = query.data;
if (!data) return;
if (!data || data.ref === -1) return;
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;
lastValidTimestamp.current = now;
@@ -110,11 +98,6 @@ export function useSightingFeed(url: string | undefined) {
setSelectedRef(data.ref);
}, [query.data]);
useEffect(() => {
if (query.error) {
// console.error("Sighting feed error:", query.error);
}
}, [query.error]);
return {
sightings,
selectedRef,

View File

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

View File

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