From 40909d48b6541cc70c610c5c1349056ad13ee86a Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 8 Oct 2025 11:08:41 +0100 Subject: [PATCH 1/5] - added state to set sound set settings for sightings and NPED hits - added function to save mute settings --- .../Sound/SoundSettingsFields.tsx | 26 ++++++++++++------- src/components/UI/SoundBtn.tsx | 16 ++++++++++-- .../providers/SoundContextProvider.tsx | 17 +++++++++++- src/context/reducers/SoundContextReducer.ts | 2 +- src/hooks/useCameraBlackboard.ts | 3 +-- src/hooks/useSightingFeed.ts | 2 +- src/hooks/useSound.ts | 0 vite.config.ts | 10 ++++++- 8 files changed, 59 insertions(+), 17 deletions(-) delete mode 100644 src/hooks/useSound.ts diff --git a/src/components/SettingForms/Sound/SoundSettingsFields.tsx b/src/components/SettingForms/Sound/SoundSettingsFields.tsx index af5e8f1..3cc4d6f 100644 --- a/src/components/SettingForms/Sound/SoundSettingsFields.tsx +++ b/src/components/SettingForms/Sound/SoundSettingsFields.tsx @@ -2,15 +2,14 @@ import { Field, FieldArray, Form, Formik } from "formik"; import FormGroup from "../components/FormGroup"; import type { FormValues, Hotlist } from "../../../types/types"; import { useSoundContext } from "../../../context/SoundContext"; +import { useCameraBlackboard } from "../../../hooks/useCameraBlackboard"; import { toast } from "sonner"; const SoundSettingsFields = () => { const { state, dispatch } = useSoundContext(); - const hotlists: Hotlist[] = [ - { name: "hotlist0", sound: "" }, - { name: "hotlist1", sound: "" }, - { name: "hotlist2", sound: "" }, - ]; + const { mutation } = useCameraBlackboard(); + + const hotlists: Hotlist[] = state.hotlists; const soundOptions = state?.soundOptions?.map((soundOption) => ({ value: soundOption?.name, @@ -23,9 +22,18 @@ const SoundSettingsFields = () => { hotlists, }; - const handleSubmit = (values: FormValues) => { + const handleSubmit = async (values: FormValues) => { dispatch({ type: "UPDATE", payload: values }); - toast.success("Sound settings updated"); + const result = await mutation.mutateAsync({ + operation: "INSERT", + path: "soundSettings", + value: values, + }); + if (result.reason !== "OK") { + toast.error("Cannot update sound settings"); + } else { + toast.success("Sound Settings successfully updated"); + } }; return ( @@ -68,8 +76,8 @@ const SoundSettingsFields = () => { name="hotlists" render={() => (
- {values.hotlists.length > 0 ? ( - values.hotlists.map((hotlist, index) => ( + {values?.hotlists?.length > 0 ? ( + values?.hotlists?.map((hotlist, index) => (
{ + const { mutation, query } = useCameraBlackboard(); const [enabled, setEnabled] = useSoundEnabled(); - const handleClick = () => { - setEnabled(!enabled); + const handleClick = async () => { + const newEnabled = !enabled; + setEnabled(newEnabled); + await mutation.mutateAsync({ + operation: "INSERT", + path: "soundEnabled", + value: { enabled: newEnabled }, + }); }; + useEffect(() => { + setEnabled(query?.data?.soundEnabled?.enabled); + }, [query?.data?.soundEnabled?.enabled, setEnabled]); return (
    -
  • Number of Vehicles: {sessionList.length}
  • -
  • Vehicles without Tax:
  • -
  • Vehicles without MOT:
  • -
  • Vehicles with NPED Cat A:
  • -
  • Vehicles with NPED Cat B:
  • -
  • Vehicles with NPED Cat C:
  • +
  • Number of Vehicles: {dedupedSightings.length}
  • +
  • Vehicles without Tax: {vehicles.notTaxed.length}
  • +
  • Vehicles without MOT: {vehicles.notMOT.length}
  • +
  • Vehicles with NPED Cat A: {vehicles.npedCatA.length}
  • +
  • Vehicles with NPED Cat B: {vehicles.npedCatB.length}
  • +
  • Vehicles with NPED Cat C: {vehicles.npedCatC.length}
diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 3305233..754e03e 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { SightingType } from "../../types/types"; +import type { ReducedSightingType, SightingType } from "../../types/types"; import { BLANK_IMG, getSoundFileURL } from "../../utils/utils"; import NumberPlate from "../PlateStack/NumberPlate"; import Card from "../UI/Card"; @@ -61,10 +61,18 @@ export default function SightingHistoryWidget({ const { dispatch } = useAlertHitContext(); const { sessionStarted, setSessionList, sessionList } = useNPEDContext(); + const reduceObject = (obj: SightingType): ReducedSightingType => { + return { + vrm: obj.vrm, + metadata: obj?.metadata, + }; + }; + useEffect(() => { if (sessionStarted) { if (!mostRecent) return; - setSessionList([...sessionList, mostRecent]); + const reducedMostRecent = reduceObject(mostRecent); + setSessionList([...sessionList, reducedMostRecent]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mostRecent, sessionStarted, setSessionList]); diff --git a/src/context/NPEDUserContext.ts b/src/context/NPEDUserContext.ts index 749fd7d..7b36b9e 100644 --- a/src/context/NPEDUserContext.ts +++ b/src/context/NPEDUserContext.ts @@ -1,13 +1,13 @@ import { createContext, useContext, type SetStateAction } from "react"; -import type { NPEDUser, SightingType } from "../types/types"; +import type { NPEDUser, ReducedSightingType } from "../types/types"; type UserContextValue = { user: NPEDUser | null; setUser: React.Dispatch>; sessionStarted: boolean; setSessionStarted: React.Dispatch>; - sessionList: SightingType[]; - setSessionList: React.Dispatch>; + sessionList: ReducedSightingType[]; + setSessionList: React.Dispatch>; }; export const NPEDUserContext = createContext( diff --git a/src/context/providers/NPEDUserContextProvider.tsx b/src/context/providers/NPEDUserContextProvider.tsx index 2a15b3a..e76247f 100644 --- a/src/context/providers/NPEDUserContextProvider.tsx +++ b/src/context/providers/NPEDUserContextProvider.tsx @@ -1,5 +1,5 @@ import { useState, type ReactNode } from "react"; -import type { NPEDUser, SightingType } from "../../types/types"; +import type { NPEDUser, ReducedSightingType } from "../../types/types"; import { NPEDUserContext } from "../NPEDUserContext"; type NPEDUserProviderType = { @@ -9,7 +9,7 @@ type NPEDUserProviderType = { export const NPEDUserProvider = ({ children }: NPEDUserProviderType) => { const [user, setUser] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); - const [sessionList, setSessionList] = useState([]); + const [sessionList, setSessionList] = useState([]); return ( ([]); const [selectedRef, setSelectedRef] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); - const [sessionList, setSessionList] = useState([]); const mostRecent = sightings[0] ?? null; const latestRef = mostRecent?.ref ?? null; const [selectedSighting, setSelectedSighting] = useState( @@ -76,13 +75,6 @@ export function useSightingFeed(url: string | undefined) { staleTime: 0, }); - useEffect(() => { - if (sessionStarted) { - if (!mostRecent) return; - setSessionList([...sessionList, mostRecent]); - } - }, [mostRecent, sessionList, sessionStarted]); - useEffect(() => { const data = query.data; if (!data) return; @@ -129,7 +121,6 @@ export function useSightingFeed(url: string | undefined) { setSelectedRef, mostRecent, selectedSighting, - sessionList, sessionStarted, setSessionStarted, setSelectedSighting, diff --git a/src/types/types.ts b/src/types/types.ts index 6169124..8d35242 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -30,6 +30,11 @@ export type SightingType = { metadata?: Metadata; }; +export type ReducedSightingType = { + vrm: string; + metadata?: Metadata; +}; + export type CameraSettingValues = { friendlyName: string; cameraAddress: string; @@ -103,6 +108,10 @@ export type NpedJSON = { status_code: number; reason_phrase: string; "NPED CATEGORY": "A" | "B" | "C" | "D"; + "MOT STATUS": boolean; + "TAX STATUS": boolean; + vrm: string; + "INSURANCE STATUS": string; }; export type NPEDUser = { From 063815cac0d661f6ab2994db122e60a07c683050 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Thu, 9 Oct 2025 14:11:58 +0100 Subject: [PATCH 3/5] - refactored code around hotlist hits and sounds - improved performace for sounds playing --- src/components/HistoryList/AlertItem.tsx | 6 +- .../SightingModal/SightingModal.tsx | 3 +- .../SightingsWidget/SightingWidget.tsx | 59 ++++++++++++----- src/context/SightingFeedContext.ts | 2 - .../providers/SightingFeedProvider.tsx | 4 -- src/context/reducers/SoundContextReducer.ts | 2 +- src/utils/utils.ts | 66 +++---------------- 7 files changed, 57 insertions(+), 85 deletions(-) diff --git a/src/components/HistoryList/AlertItem.tsx b/src/components/HistoryList/AlertItem.tsx index 13053d6..48c12ce 100644 --- a/src/components/HistoryList/AlertItem.tsx +++ b/src/components/HistoryList/AlertItem.tsx @@ -8,6 +8,7 @@ import { useAlertHitContext } from "../../context/AlertHitContext"; import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg"; +import { checkIsHotListHit } from "../../utils/utils"; type AlertItemProps = { item: SightingType; @@ -19,9 +20,8 @@ const AlertItem = ({ item }: AlertItemProps) => { // const {d} = useCameraBlackboard(); const motionAway = (item?.motion ?? "").toUpperCase() === "AWAY"; - // [34].metadata.hotlistMatches["MAV_Hotlist.csv"] - //check if true is in any hotlist property - const isHotListHit = item?.metadata?.hotlistMatches?.Hotlist0 === true; + + const isHotListHit = checkIsHotListHit(item); const isNPEDHitA = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "A"; const isNPEDHitB = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = item?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; diff --git a/src/components/SightingModal/SightingModal.tsx b/src/components/SightingModal/SightingModal.tsx index ad2b65e..aef4981 100644 --- a/src/components/SightingModal/SightingModal.tsx +++ b/src/components/SightingModal/SightingModal.tsx @@ -10,6 +10,7 @@ import HotListImg from "/Hotlist_Hit.svg"; import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg"; +import { checkIsHotListHit } from "../../utils/utils"; type SightingModalProps = { isSightingModalOpen: boolean; @@ -65,7 +66,7 @@ const SightingModal = ({ }; const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY"; - const isHotListHit = sighting?.metadata?.hotlistMatches?.Hotlist0 === true; + const isHotListHit = checkIsHotListHit(sighting); const isNPEDHitA = sighting?.metadata?.npedJSON?.["NPED CATEGORY"] === "A"; const isNPEDHitB = sighting?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = sighting?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 754e03e..2a9df67 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -13,10 +13,12 @@ import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg"; import popup from "../../assets/sounds/ui/popup_open.mp3"; +import notification from "../../assets/sounds/ui/notification.mp3"; import { useSound } from "react-sounds"; import { useNPEDContext } from "../../context/NPEDUserContext"; import { useSoundContext } from "../../context/SoundContext"; import Loading from "../UI/Loading"; +import { checkIsHotListHit } from "../../utils/utils"; function useNow(tickMs = 1000) { const [, setNow] = useState(() => Date.now()); @@ -43,11 +45,16 @@ export default function SightingHistoryWidget({ useNow(1000); const { state } = useSoundContext(); - const soundSrc = useMemo(() => { + const soundSrcNped = useMemo(() => { return getSoundFileURL(state.NPEDsound) ?? popup; }, [state.NPEDsound]); - const { play } = useSound(soundSrc); + const soundSrcHotlist = useMemo(() => { + return getSoundFileURL(state?.hotlists?.[0]?.sound) ?? notification; + }, [state.hotlists]); + + const { play: npedSound } = useSound(soundSrcNped); + const { play: hotlistsound } = useSound(soundSrcHotlist); const { sightings, setSelectedSighting, @@ -78,6 +85,7 @@ export default function SightingHistoryWidget({ }, [mostRecent, sessionStarted, setSessionList]); const hasAutoOpenedRef = useRef(false); + const npedRef = useRef(false); const onRowClick = useCallback( (sighting: SightingType) => { @@ -98,32 +106,52 @@ export default function SightingHistoryWidget({ const isNPEDHitA = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "A"; const isNPEDHitB = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; - - if (isNPEDHitA || isNPEDHitB || isNPEDHitC) { + const isNPEDHitD = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "D"; + if (isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD) { dispatch({ type: "ADD", payload: obj, }); } }); - }, [dispatch, rows]); + }, [dispatch]); useEffect(() => { - if (hasAutoOpenedRef.current) return; - const firstHot = rows?.find((r) => { - const isHotListHit = r?.metadata?.hotlistMatches?.Hotlist0 === true; + if (hasAutoOpenedRef.current || npedRef.current) return; + const firstNPED = rows.find((r) => { const isNPEDHitA = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "A"; const isNPEDHitB = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; - return isNPEDHitA || isNPEDHitB || isNPEDHitC || isHotListHit; + const isNPEDHitD = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "D"; + return isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD; }); + const firstHot = rows?.find((r) => { + const isHotListHit = checkIsHotListHit(r); + + return isHotListHit; + }); + + if (firstNPED) { + setSelectedSighting(firstNPED); + console.log("first"); + npedSound(); + setSightingModalOpen(true); + npedRef.current = true; + } + if (firstHot) { setSelectedSighting(firstHot); - play(); + hotlistsound(); setSightingModalOpen(true); hasAutoOpenedRef.current = true; } - }, [play, rows, setSelectedSighting, setSightingModalOpen]); + }, [ + hotlistsound, + npedSound, + rows, + setSelectedSighting, + setSightingModalOpen, + ]); const handleClose = () => { setSightingModalOpen(false); @@ -152,11 +180,8 @@ export default function SightingHistoryWidget({ obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; - const isNPEDHitD = - obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "D"; const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY"; - const isHotListHit = - obj?.metadata?.hotlistMatches?.Hotlist0 === true; + const isHotListHit = checkIsHotListHit(obj); return (
onRowClick(obj)} >
void; }; export const SightingFeedContext = createContext< diff --git a/src/context/providers/SightingFeedProvider.tsx b/src/context/providers/SightingFeedProvider.tsx index 6b6b8aa..5e0fb78 100644 --- a/src/context/providers/SightingFeedProvider.tsx +++ b/src/context/providers/SightingFeedProvider.tsx @@ -23,9 +23,7 @@ export const SightingFeedProvider = ({ setSelectedSighting, selectedSighting, mostRecent, - sessionList, sessionStarted, - setSessionStarted, } = useSightingFeed(url); const [isSightingModalOpen, setSightingModalOpen] = useState(false); @@ -45,9 +43,7 @@ export const SightingFeedProvider = ({ isLoading, side, data, - sessionList, sessionStarted, - setSessionStarted, }} > {children} diff --git a/src/context/reducers/SoundContextReducer.ts b/src/context/reducers/SoundContextReducer.ts index 42f8d2d..6250239 100644 --- a/src/context/reducers/SoundContextReducer.ts +++ b/src/context/reducers/SoundContextReducer.ts @@ -3,7 +3,7 @@ import type { SoundAction, SoundState } from "../../types/types"; export const initialState: SoundState = { sightingSound: "switch", NPEDsound: "popup", - hotlists: [], + hotlists: [{ name: "hotlistName", sound: "notification" }], soundOptions: [ { name: "switch (Default)", soundFile: null }, { name: "popup", soundFile: null }, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1c1b257..0f4488f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,6 +1,7 @@ import switchSound from "../assets/sounds/ui/switch.mp3"; import popup from "../assets/sounds/ui/popup_open.mp3"; import notification from "../assets/sounds/ui/notification.mp3"; +import type { SightingType } from "../types/types"; export function getSoundFileURL(name: string) { const sounds: Record = { @@ -129,59 +130,12 @@ export function drawRects( }); } -// 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]); +export const checkIsHotListHit = (sigthing: SightingType | null) => { + if (!sigthing) return; + if (sigthing?.metadata?.hotlistMatches) { + const isHotListHit = Object.values( + sigthing?.metadata?.hotlistMatches + ).includes(true); + return isHotListHit; + } +}; From 0a74ebfbfed503646da67841b0ff0339342f668b Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Thu, 9 Oct 2025 14:14:33 +0100 Subject: [PATCH 4/5] removed nped cat d tag --- src/components/SightingsWidget/InfoBar.tsx | 1 - src/components/SightingsWidget/SightingWidget.tsx | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/SightingsWidget/InfoBar.tsx b/src/components/SightingsWidget/InfoBar.tsx index c9ff970..b19f192 100644 --- a/src/components/SightingsWidget/InfoBar.tsx +++ b/src/components/SightingsWidget/InfoBar.tsx @@ -5,7 +5,6 @@ type InfoBarprops = { obj: SightingType; }; const InfoBar = ({ obj }: InfoBarprops) => { - // const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 404; const isNPEDHitD = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "D"; return ( diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 2a9df67..1e730f7 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -106,8 +106,8 @@ export default function SightingHistoryWidget({ const isNPEDHitA = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "A"; const isNPEDHitB = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; - const isNPEDHitD = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "D"; - if (isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD) { + + if (isNPEDHitA || isNPEDHitB || isNPEDHitC) { dispatch({ type: "ADD", payload: obj, @@ -122,8 +122,7 @@ export default function SightingHistoryWidget({ const isNPEDHitA = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "A"; const isNPEDHitB = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "B"; const isNPEDHitC = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; - const isNPEDHitD = r?.metadata?.npedJSON?.["NPED CATEGORY"] === "D"; - return isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD; + return isNPEDHitA || isNPEDHitB || isNPEDHitC; }); const firstHot = rows?.find((r) => { const isHotListHit = checkIsHotListHit(r); From 9f3674e460506bf4834630a1cb0c26004ae62671 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Thu, 9 Oct 2025 14:21:59 +0100 Subject: [PATCH 5/5] -added cam base for api calls --- src/hooks/useCameraBlackboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useCameraBlackboard.ts b/src/hooks/useCameraBlackboard.ts index e807831..487b641 100644 --- a/src/hooks/useCameraBlackboard.ts +++ b/src/hooks/useCameraBlackboard.ts @@ -15,7 +15,7 @@ const getAllBlackboardData = async () => { }; const viewBlackboardData = async (options: CameraBlackBoardOptions) => { - const response = await fetch(`/api/blackboard`, { + const response = await fetch(`${CAM_BASE}/api/blackboard`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(options),