diff --git a/.env b/.env index a8fa5c2..28c2d7a 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ VITE_BASEURL=http://192.168.75.11/ VITE_CAM_BASE=http://100.113.222.39 VITE_FOLKESTONE_BASE=http://100.116.253.81 VITE_TESTURL=http://100.82.205.44/SightingListRear/sightingSummary?mostRecentRef=-1 -VITE_OUTSIDE_BASEURL=http://100.82.205.44/api +VITE_OUTSIDE_BASEURL=http://100.82.205.44 VITE_FOLKESTONE_URL=http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef= VITE_MAV_URL=http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef= diff --git a/src/App.tsx b/src/App.tsx index e835576..aeca601 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,23 +7,26 @@ import SystemSettings from "./pages/SystemSettings"; import Session from "./pages/Session"; import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider"; import { AlertHitProvider } from "./context/providers/AlertHitProvider"; +import { SoundProvider } from "react-sounds"; function App() { return ( - - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - - - - + + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); } diff --git a/src/assets/sounds/ui/computer-mouse-click.mp3 b/src/assets/sounds/ui/computer-mouse-click.mp3 new file mode 100644 index 0000000..bb8e237 Binary files /dev/null and b/src/assets/sounds/ui/computer-mouse-click.mp3 differ diff --git a/src/assets/sounds/ui/keystroke_hard.mp3 b/src/assets/sounds/ui/keystroke_hard.mp3 new file mode 100644 index 0000000..128ef88 Binary files /dev/null and b/src/assets/sounds/ui/keystroke_hard.mp3 differ diff --git a/src/assets/sounds/ui/popup_open.mp3 b/src/assets/sounds/ui/popup_open.mp3 new file mode 100644 index 0000000..bf3a176 Binary files /dev/null and b/src/assets/sounds/ui/popup_open.mp3 differ diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index a30fe34..3f88cf9 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -21,13 +21,13 @@ const CameraSettingFields = ({ const initialValues = useMemo( () => ({ - friendlyName: initialData?.propLEDDriverControlURI?.value ?? "", - cameraAddress: "", + friendlyName: initialData?.id ?? "", + cameraAddress: initialData?.propURI?.value ?? "", userName: "", password: "", id: initialData?.id, }), - [initialData?.id, initialData?.propLEDDriverControlURI?.value] + [initialData?.id, initialData?.propURI?.value] ); const validateValues = (values: CameraSettingValues) => { diff --git a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx index 81c2ddb..2bd572e 100644 --- a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx +++ b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx @@ -6,6 +6,7 @@ import { useSwipeable } from "react-swipeable"; import { useNavigate } from "react-router"; import { useOverviewVideo } from "../../hooks/useOverviewVideo"; import SightingOverview from "../SightingOverview/SightingOverview"; +import { useSightingFeedContext } from "../../context/SightingFeedContext"; type CardProps = React.HTMLAttributes; @@ -17,6 +18,7 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => { trackMouse: true, }); + const { mostRecent } = useSightingFeedContext(); return ( { )} > - + {/* */} diff --git a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx index 40421f1..55afe4e 100644 --- a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx +++ b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx @@ -6,6 +6,7 @@ import { useNavigate } from "react-router"; import CardHeader from "../UI/CardHeader"; import { faCamera } from "@fortawesome/free-regular-svg-icons"; import SightingOverview from "../SightingOverview/SightingOverview"; +import { useSightingFeedContext } from "../../context/SightingFeedContext"; type CardProps = React.HTMLAttributes; @@ -15,7 +16,7 @@ const RearCameraOverviewCard = ({ className }: CardProps) => { onSwipedLeft: () => navigate("/rear-camera-settings"), trackMouse: true, }); - + const { mostRecent } = useSightingFeedContext(); return ( { )} > - + diff --git a/src/components/SessionForm/SessionCard.tsx b/src/components/SessionForm/SessionCard.tsx index 675094c..1502bb5 100644 --- a/src/components/SessionForm/SessionCard.tsx +++ b/src/components/SessionForm/SessionCard.tsx @@ -1,10 +1,12 @@ +import { useSound } from "react-sounds"; import Card from "../UI/Card"; import CardHeader from "../UI/CardHeader"; const SessionCard = () => { - function onStart(): void { - throw new Error("Function not implemented."); - } + const { play } = useSound("notification/notification"); + // function onStart(): void { + // throw new Error("Function not implemented."); + // } return ( @@ -12,7 +14,9 @@ const SessionCard = () => { { + play(); + }} > Start Session diff --git a/src/components/SettingForms/System/SettingSaveRecall.tsx b/src/components/SettingForms/System/SettingSaveRecall.tsx index 15bc00a..83511b9 100644 --- a/src/components/SettingForms/System/SettingSaveRecall.tsx +++ b/src/components/SettingForms/System/SettingSaveRecall.tsx @@ -1,4 +1,5 @@ import type { SystemValues } from "../../../types/types"; +import { CAM_BASE } from "../../../utils/config"; export async function handleSystemSave(values: SystemValues) { const payload = { @@ -16,7 +17,7 @@ export async function handleSystemSave(values: SystemValues) { }; try { - const response = await fetch("http://192.168.75.11/api/update-config", { + const response = await fetch(`${CAM_BASE}/api/update-config`, { method: "POST", headers: { "Content-Type": "application/json", @@ -39,7 +40,7 @@ export async function handleSystemSave(values: SystemValues) { } export async function handleSystemRecall() { - const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device"; + const url = `${CAM_BASE}/api/fetch-config?id=GLOBAL--Device`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 7000); diff --git a/src/components/SettingForms/System/SystemConfigFields.tsx b/src/components/SettingForms/System/SystemConfigFields.tsx index 687eb83..1b14594 100644 --- a/src/components/SettingForms/System/SystemConfigFields.tsx +++ b/src/components/SettingForms/System/SystemConfigFields.tsx @@ -8,7 +8,6 @@ import { useSystemConfig } from "../../../hooks/useSystemConfig"; const SystemConfigFields = () => { const { saveSystemSettings, systemSettingsData } = useSystemConfig(); - const initialvalues: SystemValues = { deviceName: systemSettingsData?.deviceName ?? "", timeZone: systemSettingsData?.timeZone ?? "", diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index a6f0dc6..df1c816 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -13,6 +13,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 popup from "../../assets/sounds/ui/popup_open.mp3"; import { useSound } from "react-sounds"; function useNow(tickMs = 1000) { @@ -38,7 +39,7 @@ export default function SightingHistoryWidget({ title, }: SightingHistoryProps) { useNow(1000); - const { play } = useSound("notification/notification"); + const { play } = useSound(popup); const { sightings, setSelectedSighting, @@ -72,14 +73,13 @@ export default function SightingHistoryWidget({ const isNPEDHitC = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "C"; if (isNPEDHitA || isNPEDHitB || isNPEDHitC) { - play(); dispatch({ type: "ADD", payload: obj, }); } }); - }, [rows, dispatch, play]); + }, [dispatch, rows]); useEffect(() => { if (hasAutoOpenedRef.current) return; diff --git a/src/components/SightingsWidget/SightingWidgetDetails.tsx b/src/components/SightingsWidget/SightingWidgetDetails.tsx index 5d35a74..642f1ba 100644 --- a/src/components/SightingsWidget/SightingWidgetDetails.tsx +++ b/src/components/SightingsWidget/SightingWidgetDetails.tsx @@ -1,5 +1,4 @@ import type { SightingType } from "../../types/types"; -import { useState } from "react"; type SightingWidgetDetailsProps = { effectiveSelected: SightingType | null; @@ -8,85 +7,38 @@ type SightingWidgetDetailsProps = { const SightingWidgetDetails = ({ effectiveSelected, }: SightingWidgetDetailsProps) => { - const [advancedDetailsEnabled, setAdvancedDetailsEnabled] = useState(false); - - const handleDetailsClick = () => - setAdvancedDetailsEnabled(!advancedDetailsEnabled); - return ( <> - - VRM:{" "} - {effectiveSelected?.vrm ?? "—"} - - - - Make:{" "} - {effectiveSelected?.make ?? "—"} - - - Model:{" "} - {effectiveSelected?.model ?? "—"} - - - Colour:{" "} - {effectiveSelected?.color ?? "—"} - - - Timestamp:{" "} - - {effectiveSelected?.timeStamp ?? "—"} - - - {advancedDetailsEnabled && ( - <> - - Country:{" "} - - {effectiveSelected?.countryCode ?? "—"} - - - - Seen:{" "} - - {effectiveSelected?.seenCount ?? "—"} - - - - Category:{" "} - - {effectiveSelected?.category ?? "—"} - - - - Char Ht:{" "} - - {effectiveSelected?.charHeight ?? "—"} - - - - Plate Size:{" "} - - {effectiveSelected?.plateSize ?? "—"} - - - - Overview Size:{" "} - - {effectiveSelected?.overviewSize ?? "—"} - - - > + {effectiveSelected?.vrm && ( + + VRM:{" "} + {effectiveSelected?.vrm ?? "—"} + + )} + + {effectiveSelected?.make !== "" && ( + + Make:{" "} + {effectiveSelected?.make ?? "—"} + + )} + {effectiveSelected?.model.trim() !== "" && ( + + Model:{" "} + + {effectiveSelected?.model ?? "—"} + + + )} + {effectiveSelected?.color !== "" && ( + + Colour:{" "} + + {effectiveSelected?.color ?? "—"} + + )} - - - - Sighting Details - > ); diff --git a/src/components/UI/CardHeader.tsx b/src/components/UI/CardHeader.tsx index df8724a..120df69 100644 --- a/src/components/UI/CardHeader.tsx +++ b/src/components/UI/CardHeader.tsx @@ -1,18 +1,27 @@ import type { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; +import NumberPlate from "../PlateStack/NumberPlate"; +import type { SightingType } from "../../types/types"; type CameraOverviewHeaderProps = { title: string; icon?: IconProp; img?: string; + sighting?: SightingType | null; }; -const CardHeader = ({ title, icon, img }: CameraOverviewHeaderProps) => { +const CardHeader = ({ + title, + icon, + img, + sighting, +}: CameraOverviewHeaderProps) => { + // console.log(sighting?.debug.toLowerCase()); return ( @@ -22,6 +31,7 @@ const CardHeader = ({ title, icon, img }: CameraOverviewHeaderProps) => { {img && ( )} + {sighting?.vrm && } ); }; diff --git a/src/components/UI/Header.tsx b/src/components/UI/Header.tsx index c38270b..d959e63 100644 --- a/src/components/UI/Header.tsx +++ b/src/components/UI/Header.tsx @@ -10,6 +10,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import type { VersionFieldType } from "../../types/types"; import { useEffect, useState } from "react"; +import SoundBtn from "./SoundBtn"; async function fetchVersions( signal?: AbortSignal @@ -120,6 +121,7 @@ export default function Header() { size="2x" /> + diff --git a/src/components/UI/SoundBtn.tsx b/src/components/UI/SoundBtn.tsx new file mode 100644 index 0000000..fb2080f --- /dev/null +++ b/src/components/UI/SoundBtn.tsx @@ -0,0 +1,22 @@ +import { faVolumeHigh, faVolumeXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useSoundEnabled } from "react-sounds"; + +const SoundBtn = () => { + const [enabled, setEnabled] = useSoundEnabled(); + + const handleClick = () => { + setEnabled(!enabled); + }; + + return ( + + + + ); +}; + +export default SoundBtn; diff --git a/src/context/SightingFeedContext.ts b/src/context/SightingFeedContext.ts index 2cddb67..493fde6 100644 --- a/src/context/SightingFeedContext.ts +++ b/src/context/SightingFeedContext.ts @@ -14,6 +14,7 @@ type SightingFeedContextType = { isSightingModalOpen: boolean; isError: boolean; isLoading: boolean; + data: SightingType | undefined; }; export const SightingFeedContext = createContext< diff --git a/src/context/providers/SightingFeedProvider.tsx b/src/context/providers/SightingFeedProvider.tsx index bfc4f33..c903ede 100644 --- a/src/context/providers/SightingFeedProvider.tsx +++ b/src/context/providers/SightingFeedProvider.tsx @@ -17,13 +17,13 @@ export const SightingFeedProvider = ({ sightings, selectedRef, setSelectedRef, - + data, isLoading, isError, setSelectedSighting, selectedSighting, mostRecent, - } = useSightingFeed(url); + } = useSightingFeed(url, side); const [isSightingModalOpen, setSightingModalOpen] = useState(false); @@ -41,6 +41,7 @@ export const SightingFeedProvider = ({ isError, isLoading, side, + data, }} > {children} diff --git a/src/hooks/useCameraConfig.ts b/src/hooks/useCameraConfig.ts index afd985a..284468f 100644 --- a/src/hooks/useCameraConfig.ts +++ b/src/hooks/useCameraConfig.ts @@ -1,7 +1,9 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; +import { CAM_BASE } from "../utils/config"; -const base_url = import.meta.env.VITE_OUTSIDE_BASEURL; +const base_url = `${CAM_BASE}/api`; +console.log(base_url); const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => { const [, cameraSide] = queryKey; diff --git a/src/hooks/useGetOverviewSnapshot.ts b/src/hooks/useGetOverviewSnapshot.ts index d872659..66dcc7b 100644 --- a/src/hooks/useGetOverviewSnapshot.ts +++ b/src/hooks/useGetOverviewSnapshot.ts @@ -5,7 +5,6 @@ import { CAM_BASE } from "../utils/config"; const apiUrl = CAM_BASE; async function fetchSnapshot(cameraSide: string) { - console.log(`${apiUrl}/${cameraSide}-preview`); const response = await fetch(`${apiUrl}/${cameraSide}-preview`); if (!response.ok) { throw new Error("Cannot reach endpoint"); diff --git a/src/hooks/useNPEDAuth.ts b/src/hooks/useNPEDAuth.ts index 96fab80..ed48fc1 100644 --- a/src/hooks/useNPEDAuth.ts +++ b/src/hooks/useNPEDAuth.ts @@ -2,10 +2,10 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import type { NPEDFieldType } from "../types/types"; import { useNPEDContext } from "../context/NPEDUserContext"; import { useEffect } from "react"; +import { CAM_BASE } from "../utils/config"; -const base_url = import.meta.env.VITE_OUTSIDE_BASEURL; async function fetchNPEDDetails() { - const fetchUrl = `${base_url}/fetch-config?id=NPED`; + const fetchUrl = `${CAM_BASE}/api/fetch-config?id=NPED`; const response = await fetch(fetchUrl); if (!response.ok) throw new Error("Cannot reach fetch-config endpoint"); @@ -14,8 +14,8 @@ async function fetchNPEDDetails() { async function signIn(loginDetails: NPEDFieldType) { const { frontId, rearId, username, password, clientId } = loginDetails; - const NPEDLoginURLFront = `${base_url}/update-config?id=${frontId}`; - const NPEDLoginURLRear = `${base_url}/update-config?id=${rearId}`; + const NPEDLoginURLFront = `${CAM_BASE}/api/update-config?id=${frontId}`; + const NPEDLoginURLRear = `${CAM_BASE}/api/update-config?id=${rearId}`; const frontCameraPayload = { id: frontId, fields: [ @@ -66,7 +66,7 @@ async function signOut() { { property: "propClientID", value: "" }, ], }; - const NPEDLoginURLFront = `${base_url}/update-config?id=NPED`; + const NPEDLoginURLFront = `${CAM_BASE}/api/update-config?id=NPED`; const response = await fetch(NPEDLoginURLFront, { method: "POST", body: JSON.stringify(nullPayload), diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index b81f036..6ca2715 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { SightingType } from "../types/types"; +import { useSoundOnChange } from "react-sounds"; +import click from "../assets/sounds/ui/computer-mouse-click.mp3"; async function fetchSighting(url: string, ref: number): Promise { const res = await fetch(`${url}${ref}`); @@ -8,14 +10,19 @@ async function fetchSighting(url: string, ref: number): Promise { return res.json(); } -export function useSightingFeed(url: string) { +export function useSightingFeed(url: string, side: string) { const [sightings, setSightings] = useState([]); const [selectedRef, setSelectedRef] = useState(null); const mostRecent = sightings[0] ?? null; + const latestRef = mostRecent?.ref ?? null; const [selectedSighting, setSelectedSighting] = useState( null ); + useSoundOnChange(click, latestRef, { + volume: side === "Rear" ? 0 : 1, + }); + const currentRef = useRef(-1); const lastValidTimestamp = useRef(Date.now()); @@ -61,6 +68,13 @@ export function useSightingFeed(url: string) { return; } + // if (Notification.permission === "granted") { + // new Notification("New Sighting!", { + // body: `Ref: ${data.ref}`, + // icon: "/MAV-blue.svg", + // }); + // } + currentRef.current = data.ref; lastValidTimestamp.current = now; @@ -75,7 +89,6 @@ export function useSightingFeed(url: string) { useEffect(() => { if (query.error) { - // you can add logging/telemetry here // console.error("Sighting feed error:", query.error); } }, [query.error]); diff --git a/src/hooks/useSound.ts b/src/hooks/useSound.ts new file mode 100644 index 0000000..21f3455 --- /dev/null +++ b/src/hooks/useSound.ts @@ -0,0 +1,64 @@ +// useBeep.ts +import { useEffect, useRef } from "react"; +import { useSoundEnabled } from "react-sounds"; // so it respects your SoundBtn toggle + +/** + * Plays a sound whenever `latestRef` changes. + * + * @param src Path to the sound file + * @param latestRef The primitive value to watch (e.g. sighting.ref) + * @param opts volume: 0..1, enabledOverride: force enable/disable, minGapMs: throttle interval + */ +export function useBeep( + src: string, + latestRef: number | null, + opts?: { volume?: number; enabledOverride?: boolean; minGapMs?: number } +) { + const audioRef = useRef(undefined); + const prevRef = useRef(null); + const lastPlay = useRef(0); + const [enabled] = useSoundEnabled(); + + const minGap = opts?.minGapMs ?? 250; // don’t play more than 4 times/sec + + // Create the audio element once + useEffect(() => { + const a = new Audio(src); + a.preload = "auto"; + if (opts?.volume !== undefined) a.volume = opts.volume; + audioRef.current = a; + return () => { + a.pause(); + }; + }, [src, opts?.volume]); + + // Watch for ref changes + useEffect(() => { + if (latestRef == null) return; + + const canPlay = + (opts?.enabledOverride ?? enabled) && + document.visibilityState === "visible"; + if (!canPlay) { + prevRef.current = latestRef; // consume the change + return; + } + + if (prevRef.current !== null && latestRef !== prevRef.current) { + const now = Date.now(); + if (now - lastPlay.current >= minGap) { + const a = audioRef.current; + if (a) { + try { + a.currentTime = 0; // restart from beginning + void a.play(); // fire and forget + lastPlay.current = now; + } catch (err) { + console.warn("Audio play failed:", err); + } + } + } + } + prevRef.current = latestRef; + }, [latestRef, enabled, opts?.enabledOverride, minGap]); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 7bf11d3..fb45cd2 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -3,6 +3,7 @@ import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraO import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget"; import { SightingFeedProvider } from "../context/providers/SightingFeedProvider"; import { CAM_BASE } from "../utils/config"; + const Dashboard = () => { const dev_REAR_URL = `${CAM_BASE}/SightingListRear/sightingSummary?mostRecentRef=`; const dev_FRONT_URL = `${CAM_BASE}/SightingListFront/sightingSummary?mostRecentRef=`; diff --git a/src/pages/FrontCamera.tsx b/src/pages/FrontCamera.tsx index 712d825..c665d0a 100644 --- a/src/pages/FrontCamera.tsx +++ b/src/pages/FrontCamera.tsx @@ -12,15 +12,14 @@ const FrontCamera = () => { }); return ( - - + + + + diff --git a/src/pages/RearCamera.tsx b/src/pages/RearCamera.tsx index a450a16..3ee433d 100644 --- a/src/pages/RearCamera.tsx +++ b/src/pages/RearCamera.tsx @@ -12,16 +12,16 @@ const RearCamera = () => { }); return ( - + - + + + + ); diff --git a/src/utils/config.ts b/src/utils/config.ts index 6665faa..fac7887 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,5 @@ -const rawCamBase = import.meta.env.VITE_CAM_BASE; +// const rawCamBase = import.meta.env.VITE_CAM_BASE; +const rawCamBase = import.meta.env.VITE_OUTSIDE_BASEURL; export const CAM_BASE = rawCamBase && rawCamBase.trim().length > 0
- Sighting Details -