diff --git a/.env b/.env index 1c3211a..b3bf713 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_BASEURL=http://192.168.75.11/ \ No newline at end of file +VITE_BASEURL=http://192.168.75.11/ +VITE_TESTURL=http://100.82.205.44/SightingListRear/sightingSummary?mostRecentRef=-1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..1cac559 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env \ No newline at end of file diff --git a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx index 274303b..8c543ed 100644 --- a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx +++ b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx @@ -14,6 +14,7 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => { const navigate = useNavigate(); const handlers = useSwipeable({ onSwipedRight: () => navigate("/front-camera-settings"), + onSwipedDown: () => navigate("/system-settings"), trackMouse: true, }); diff --git a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx index bbb8f34..3b82fe3 100644 --- a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx +++ b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx @@ -1,10 +1,11 @@ import clsx from "clsx"; import Card from "../UI/Card"; -import { SnapshotContainer } from "../CameraOverview/SnapshotContainer"; +// import { SnapshotContainer } from "../CameraOverview/SnapshotContainer"; import { useSwipeable } from "react-swipeable"; import { useNavigate } from "react-router"; import CardHeader from "../UI/CardHeader"; import { faCamera } from "@fortawesome/free-regular-svg-icons"; +import SightingOverview from "../SightingOverview/SightingOverview"; type CardProps = React.HTMLAttributes; @@ -12,14 +13,21 @@ const RearCameraOverviewCard = ({ className }: CardProps) => { const navigate = useNavigate(); const handlers = useSwipeable({ onSwipedLeft: () => navigate("/rear-camera-settings"), + onSwipedDown: () => navigate("/system-settings"), trackMouse: true, }); return ( - +
- + + {/* */}
); diff --git a/src/components/SettingForms/NPED/NPEDFields.tsx b/src/components/SettingForms/NPED/NPEDFields.tsx index cc4d33f..ddc6cdb 100644 --- a/src/components/SettingForms/NPED/NPEDFields.tsx +++ b/src/components/SettingForms/NPED/NPEDFields.tsx @@ -1,8 +1,11 @@ import { Form, Formik, Field } from "formik"; import FormGroup from "../components/FormGroup"; import type { NPEDFieldType } from "../../../types/types"; +import { useNPEDAuth } from "../../../hooks/useNPEDAuh"; const NPEDFields = () => { + const { signIn, isError } = useNPEDAuth(); + const initialValues = { username: "", password: "", @@ -10,7 +13,8 @@ const NPEDFields = () => { }; const handleSubmit = (values: NPEDFieldType) => { - alert(JSON.stringify(values)); + console.log(isError); + signIn(values); }; return ( diff --git a/src/components/SightingOverview/SightingOverview.tsx b/src/components/SightingOverview/SightingOverview.tsx index aea617c..07b54e5 100644 --- a/src/components/SightingOverview/SightingOverview.tsx +++ b/src/components/SightingOverview/SightingOverview.tsx @@ -5,8 +5,15 @@ import { useOverviewOverlay } from "../../hooks/useOverviewOverlay"; import { useSightingFeedContext } from "../../context/SightingFeedContext"; import { useHiDPICanvas } from "../../hooks/useHiDPICanvas"; import NavigationArrow from "../UI/NavigationArrow"; +import { useSwipeable } from "react-swipeable"; +import { useNavigate } from "react-router"; const SightingOverview = () => { + const navigate = useNavigate(); + const handlers = useSwipeable({ + onSwipedRight: () => navigate("/front-camera-settings"), + trackMouse: true, + }); const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0); const imgRef = useRef(null); @@ -16,20 +23,17 @@ const SightingOverview = () => { setOverlayMode((m) => ((m + 1) % 3) as 0 | 1 | 2); }, []); - const { effectiveSelected } = useSightingFeedContext(); + const { effectiveSelected, side, mostRecent, noSighting } = + useSightingFeedContext(); - useOverviewOverlay(effectiveSelected, overlayMode, imgRef, canvasRef); + useOverviewOverlay(mostRecent, overlayMode, imgRef, canvasRef); const { sync } = useHiDPICanvas(imgRef, canvasRef); + if (noSighting) return

loading

; return (
- {/*
-
{effectiveSelected?.vrm ?? "—"}
-
{effectiveSelected?.countryCode ?? "—"}
-
{effectiveSelected?.timeStamp ?? "—"}
-
*/} - -
+
+
{ sync(); setOverlayMode((m) => m); }} - src={effectiveSelected?.overviewUrl || BLANK_IMG} + src={mostRecent?.overviewUrl || BLANK_IMG} alt="overview" className="absolute inset-0 w-full h-full object-contain cursor-pointer z-10" onClick={onOverviewClick} style={{ - display: effectiveSelected?.overviewUrl ? "block" : "none", + display: mostRecent?.overviewUrl ? "block" : "none", }} /> { @@ -39,8 +39,8 @@ export default function SightingHistoryWidget({ ); const rows = useMemo( - () => items.filter(Boolean) as SightingWidgetType[], - [items] + () => sightings?.filter(Boolean) as SightingWidgetType[], + [sightings] ); return ( @@ -49,7 +49,7 @@ export default function SightingHistoryWidget({
{/* Rows */}
- {rows.map((obj, idx) => { + {rows?.map((obj, idx) => { const isSelected = obj?.ref === selectedRef; const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY"; const primaryIsColour = obj?.srcCam === 1; diff --git a/src/components/UI/NavigationArrow.tsx b/src/components/UI/NavigationArrow.tsx index a3bb3b2..7a15c62 100644 --- a/src/components/UI/NavigationArrow.tsx +++ b/src/components/UI/NavigationArrow.tsx @@ -16,9 +16,9 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { return; } - if (side === "TargetDetectionFront") { + if (side === "Front") { navigate("/front-camera-settings"); - } else if (side === "TargetDetectionRear") { + } else if (side === "Rear") { navigate("/Rear-Camera-settings"); } }; @@ -42,10 +42,9 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { ); } - return ( <> - {side === "TargetDetectionFront" ? ( + {side === "Front" ? ( void; effectiveSelected: SightingWidgetType | null; + mostRecent: SightingWidgetType | null; + side: string; + isPending: boolean; + noSighting: boolean; }; type SightingFeedProviderProps = { - baseUrl: string; - entries?: number; - pollMs?: number; - autoSelectLatest?: boolean; + url: string; children: ReactNode; + side: string; }; const SightingFeedContext = createContext( @@ -22,17 +24,32 @@ const SightingFeedContext = createContext( ); export const SightingFeedProvider = ({ - baseUrl, - entries = 7, - pollMs = 500, - autoSelectLatest = true, children, + url, + side, }: SightingFeedProviderProps) => { - const { items, selectedRef, setSelectedRef, effectiveSelected } = - useSightingFeed(baseUrl, { limit: entries, pollMs, autoSelectLatest }); + const { + sightings, + selectedRef, + setSelectedRef, + effectiveSelected, + mostRecent, + isPending, + noSighting, + } = useSightingFeed(url); + return ( {children} diff --git a/src/hooks/useGetOverviewSnapshot.ts b/src/hooks/useGetOverviewSnapshot.ts index 7694f3a..0f454d4 100644 --- a/src/hooks/useGetOverviewSnapshot.ts +++ b/src/hooks/useGetOverviewSnapshot.ts @@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query"; const apiUrl = import.meta.env.VITE_BASEURL; async function fetchSnapshot(cameraSide: string) { + console.log(`${apiUrl}/${cameraSide}-preview`); const response = await fetch( // `http://100.116.253.81/Colour-preview` `${apiUrl}/${cameraSide}-preview` @@ -40,7 +41,7 @@ export function useGetOverviewSnapshot(cameraSide: string) { queryKey: ["overviewSnapshot", cameraSide], queryFn: () => fetchSnapshot(cameraSide), refetchOnWindowFocus: false, - refetchInterval: 1000, + // refetchInterval: 1000, }); useEffect(() => { diff --git a/src/hooks/useNPEDAuh.ts b/src/hooks/useNPEDAuh.ts new file mode 100644 index 0000000..258f90d --- /dev/null +++ b/src/hooks/useNPEDAuh.ts @@ -0,0 +1,49 @@ +import { useMutation } from "@tanstack/react-query"; +import type { NPEDFieldType } from "../types/types"; + +const url = "https://jsonplaceholder.typicode.com/posts"; + +async function signIn(loginDetails: NPEDFieldType) { + console.log(loginDetails); + const response = await fetch(url, { + method: "POST", + body: JSON.stringify(loginDetails), + }); + if (!response.ok) throw new Error("cannot reach NPED endpoint"); + return response.json(); +} + +async function signOut() { + const response = await fetch(url, { method: "POST" }); + if (!response.ok) throw new Error("cannot reach NPED sign out endpoint"); + return response.json(); +} + +function setUserContext(user) { + console.log(user); +} + +export const useNPEDAuth = () => { + const signInMutation = useMutation({ + mutationKey: ["NPEDSignin"], + mutationFn: signIn, + onSuccess: (data) => setUserContext(data), + }); + + const signOutMutation = useMutation({ + mutationKey: ["auth", "NPEDSignOut"], + mutationFn: signOut, + onSuccess: () => setUserContext(null), + }); + + return { + signIn: signInMutation.mutate, + signInAsync: signInMutation.mutateAsync, + isPending: signInMutation.isPending, + isError: signInMutation.isError, + error: signInMutation.error, + data: signInMutation.data, + + signOut: signOutMutation.mutate, + }; +}; diff --git a/src/hooks/useOverviewVideo.ts b/src/hooks/useOverviewVideo.ts index cb4bb49..87b3dc5 100644 --- a/src/hooks/useOverviewVideo.ts +++ b/src/hooks/useOverviewVideo.ts @@ -2,6 +2,8 @@ import { useQuery } from "@tanstack/react-query"; import { useRef } from "react"; const apiUrl = import.meta.env.VITE_BASEURL; +const FAST_MS = 200; // tab visible +const SLOW_MS = 2000; // tab hidden async function fetchOverviewImage(cameraSide: string) { const response = await fetch(`${apiUrl}${cameraSide}-preview`); @@ -14,7 +16,11 @@ export function useOverviewVideo() { const { isPending, isError, data } = useQuery({ queryKey: ["overviewVideo"], queryFn: () => fetchOverviewImage("CameraFront"), - refetchInterval: 500, + // refetchInterval: () => + // typeof document !== "undefined" && document.visibilityState === "hidden" + // ? SLOW_MS + // : FAST_MS, + // refetchIntervalInBackground: false, }); if (isPending) return; diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index fb969be..dc92028 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -1,91 +1,84 @@ import { useEffect, useMemo, useRef, useState } from "react"; import type { SightingWidgetType } from "../types/types"; +import { useQuery } from "@tanstack/react-query"; -export function useSightingFeed( - baseUrl: string, - { - limit = 7, - pollMs = 800, - autoSelectLatest = true, - }: { - limit?: number; - pollMs?: number; - autoSelectLatest?: boolean; - } = {} -) { - const [items, setItems] = useState( - () => Array(limit).fill(null) as unknown as SightingWidgetType[] +// const url = `http://100.82.205.44/SightingListFront/sightingSummary?mostRecentRef=-1`; + +async function fetchSighting(url: string, ref: number, signal?: AbortSignal) { + const dynamicUrl = `${url}${ref}`; + const res = await fetch(dynamicUrl, { signal }); + if (!res.ok) throw new Error(String(res.status)); + return (await res.json()) as SightingWidgetType; +} + +export function useSightingFeed(url: string) { + const [sightings, setSightings] = useState( + () => Array(7).fill(null) as unknown as SightingWidgetType[] ); + const [noSighting, setNoSighting] = useState(false); const [selectedRef, setSelectedRef] = useState(null); const [mostRecent, setMostRecent] = useState(null); const mostRecentRef = useRef(-1); + const lastSeenRef = useRef(null); + + const { data, isPending } = useQuery({ + queryKey: ["sighting"], + queryFn: ({ signal }) => fetchSighting(url, mostRecentRef.current, signal), + refetchInterval: 200, + refetchIntervalInBackground: true, + refetchOnWindowFocus: false, + staleTime: 0, + notifyOnChangeProps: ["data"], + }); + + useEffect(() => { + if (!data) return; + + if (data.ref === -1) { + setNoSighting(true); + } else { + setNoSighting(false); + } + if (data.ref === lastSeenRef.current) return; // duplicate payload → do nothing + lastSeenRef.current = data.ref; + + setSightings((prev) => { + const existing = prev.find((p) => p?.ref === data.ref); + const next = existing + ? prev + : [data, ...prev.filter(Boolean)].slice(0, 7); + + const stillHasSelection = + selectedRef != null && next.some((s) => s?.ref === selectedRef); + if (!stillHasSelection) { + setSelectedRef(data.ref); + } + + return next; + }); + setMostRecent(sightings[0]); + mostRecentRef.current = data.ref ?? -1; + }, [data, selectedRef, sightings]); - // effective selected (fallback to most recent) const selected = useMemo( () => selectedRef == null ? null - : items.find((x) => x?.ref === selectedRef) ?? null, - [items, selectedRef] + : sightings.find((s) => s?.ref === selectedRef) ?? null, + [sightings, selectedRef] ); + const effectiveSelected = selected ?? mostRecent ?? null; - useEffect(() => { - let delay = pollMs; - let dead = false; - const controller = new AbortController(); - - async function tick() { - try { - // Pause when tab hidden to save CPU/network - if (document.hidden) { - setTimeout(tick, Math.max(delay, 2000)); - return; - } - - const url = `http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=${mostRecentRef.current}`; - const res = await fetch(url, { signal: controller.signal }); - if (!res.ok) throw new Error(String(res.status)); - - const obj: SightingWidgetType = await res.json(); - if (obj && typeof obj.ref === "number" && obj.ref > -1) { - setItems((prev) => { - const next = [obj, ...prev].slice(0, limit); - // maintain selection if still present; otherwise select newest if allowed - const stillExists = - selectedRef != null && next.some((x) => x?.ref === selectedRef); - if (autoSelectLatest && !stillExists) { - setSelectedRef(obj.ref); - } - return next; - }); - setMostRecent(obj); - mostRecentRef.current = obj.ref; - delay = pollMs; // reset backoff on success - } - } catch { - // exponential backoff (max 10s) - delay = Math.min(delay * 2, 10000); - } finally { - if (!dead) setTimeout(tick, delay); - } - } - - const t = setTimeout(tick, pollMs); - return () => { - dead = true; - controller.abort(); - clearTimeout(t); - }; - }, [baseUrl, limit, pollMs, autoSelectLatest, selectedRef]); - return { - items, + sightings, selectedRef, setSelectedRef, mostRecent, effectiveSelected, mostRecentRef, + isPending, + noSighting, }; } diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 1773249..ad38ba5 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,29 +1,26 @@ import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard"; import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard"; -import { useNavigate } from "react-router"; -import { useSwipeable } from "react-swipeable"; + import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget"; import { SightingFeedProvider } from "../context/SightingFeedContext"; const Dashboard = () => { - const navigate = useNavigate(); - - const handlers = useSwipeable({ - onSwipedDown: () => navigate("/system-settings"), - trackMouse: true, - }); - return ( -
- +
+ - + diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b940280..4792b8c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -87,3 +87,60 @@ export function drawRects( ctx.stroke(); }); } + +// setSelectedRef(data?.ref); + +//setItems(data); + +// const selected = useMemo( +// () => +// selectedRef == null +// ? null +// : items.find((x) => x?.ref === selectedRef) ?? null, +// [items, selectedRef] +// ); +// const effectiveSelected = selected ?? mostRecent ?? null; + +// useEffect(() => { +// let delay = pollMs; +// let dead = false; +// const controller = new AbortController(); + +// async function tick() { +// try { +// // Pause when tab hidden to save CPU/network +// if (document.hidden) { +// setTimeout(tick, Math.max(delay, 2000)); +// return; +// } + +// if (obj && typeof obj.ref === "number" && obj.ref > -1) { +// setItems((prev) => { +// const next = [obj, ...prev].slice(0, limit); +// // maintain selection if still present; otherwise select newest if allowed +// const stillExists = +// selectedRef != null && next.some((x) => x?.ref === selectedRef); +// if (autoSelectLatest && !stillExists) { +// setSelectedRef(obj.ref); +// } +// return next; +// }); +// setMostRecent(obj); +// mostRecentRef.current = obj.ref; +// delay = pollMs; // reset backoff on success +// } +// } catch { +// // exponential backoff (max 10s) +// delay = Math.min(delay * 2, 10000); +// } finally { +// if (!dead) setTimeout(tick, delay); +// } +// } + +// const t = setTimeout(tick, pollMs); +// return () => { +// dead = true; +// controller.abort(); +// clearTimeout(t); +// }; +// }, [baseUrl, limit, pollMs, autoSelectLatest, selectedRef]);