@@ -5,7 +5,7 @@ import FrontCamera from "./pages/FrontCamera";
|
||||
import RearCamera from "./pages/RearCamera";
|
||||
import SystemSettings from "./pages/SystemSettings";
|
||||
import Session from "./pages/Session";
|
||||
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
|
||||
import { IntegrationsProvider } from "./context/providers/IntegrationsContextProvider";
|
||||
import { AlertHitProvider } from "./context/providers/AlertHitProvider";
|
||||
import { SoundProvider } from "react-sounds";
|
||||
import SoundContextProvider from "./context/providers/SoundContextProvider";
|
||||
@@ -14,7 +14,7 @@ function App() {
|
||||
return (
|
||||
<SoundContextProvider>
|
||||
<SoundProvider initialEnabled={true}>
|
||||
<NPEDUserProvider>
|
||||
<IntegrationsProvider>
|
||||
<AlertHitProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Container />}>
|
||||
@@ -27,7 +27,7 @@ function App() {
|
||||
</Route>
|
||||
</Routes>
|
||||
</AlertHitProvider>
|
||||
</NPEDUserProvider>
|
||||
</IntegrationsProvider>
|
||||
</SoundProvider>
|
||||
</SoundContextProvider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Card from "../UI/Card";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
import { useNPEDContext } from "../../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../../context/IntegrationsContext";
|
||||
import type { ReducedSightingType } from "../../types/types";
|
||||
import { toast } from "sonner";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -9,15 +9,18 @@ import VehicleSessionItem from "../UI/VehicleSessionItem";
|
||||
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
|
||||
|
||||
const SessionCard = () => {
|
||||
const { sessionStarted, setSessionStarted, sessionList, setSessionPaused, sessionPaused, savedSightings } =
|
||||
useNPEDContext();
|
||||
const { state, dispatch } = useIntegrationsContext();
|
||||
const { mutation } = useCameraBlackboard();
|
||||
|
||||
const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))];
|
||||
const sessionStarted = state.sessionStarted;
|
||||
const sessionPaused = state.sessionPaused;
|
||||
const sessionList = state.sessionList;
|
||||
|
||||
const sightings = [...new Map(sessionList?.map((vehicle) => [vehicle.vrm, vehicle]))];
|
||||
|
||||
const dedupedSightings = sightings.map((sighting) => sighting[1]);
|
||||
|
||||
const vehicles = savedSightings.reduce<Record<string, ReducedSightingType[]>>(
|
||||
const vehicles = dedupedSightings.reduce<Record<string, ReducedSightingType[]>>(
|
||||
(acc, item) => {
|
||||
const hotlisthit = Object.values(item.metadata?.hotlistMatches ?? {}).includes(true);
|
||||
if (item.metadata?.npedJSON["NPED CATEGORY"] === "A") acc.npedCatA.push(item);
|
||||
@@ -43,13 +46,13 @@ const SessionCard = () => {
|
||||
);
|
||||
|
||||
const handleStartClick = () => {
|
||||
setSessionStarted(!sessionStarted);
|
||||
setSessionPaused(false);
|
||||
dispatch({ type: "SESSIONSTART", payload: !sessionStarted });
|
||||
dispatch({ type: "SESSIONPAUSE", payload: false });
|
||||
toast(`${sessionStarted ? "Vehicle tracking session ended" : "Vehicle tracking session started"}`);
|
||||
};
|
||||
|
||||
const handlepauseClick = () => {
|
||||
setSessionPaused(!sessionPaused);
|
||||
dispatch({ type: "SESSIONPAUSE", payload: !sessionPaused });
|
||||
toast(`${sessionStarted ? "Vehicle tracking session paused" : "Vehicle tracking session resumed"}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,16 +6,18 @@ import { toast } from "sonner";
|
||||
import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useState } from "react";
|
||||
import { useIntegrationsContext } from "../../../context/IntegrationsContext";
|
||||
|
||||
const NPEDFields = () => {
|
||||
const { state } = useIntegrationsContext();
|
||||
const [showPwd, setShowPwd] = useState(false);
|
||||
const { signIn, user, signOut } = useNPEDAuth();
|
||||
const { signIn, signOut } = useNPEDAuth();
|
||||
|
||||
const initialValues = user
|
||||
const initialValues = state.npedUser
|
||||
? {
|
||||
username: user?.propUsername?.value,
|
||||
password: user?.propPassword?.value,
|
||||
clientId: user?.propClientID?.value,
|
||||
username: state.npedUser?.propUsername?.value,
|
||||
password: state.npedUser?.propPassword?.value,
|
||||
clientId: state.npedUser?.propClientID?.value,
|
||||
frontId: "NPED",
|
||||
rearId: "NPED",
|
||||
}
|
||||
@@ -48,20 +50,13 @@ const NPEDFields = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validate={validateValues}
|
||||
enableReinitialize
|
||||
>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit} validate={validateValues} enableReinitialize>
|
||||
{({ errors, touched, isSubmitting }) => (
|
||||
<Form className="flex flex-col space-y-5 px-2">
|
||||
<FormGroup>
|
||||
<label htmlFor="username">Username</label>
|
||||
{touched.username && errors.username && (
|
||||
<small className="absolute right-0 -top-5 text-red-500">
|
||||
{errors.username}
|
||||
</small>
|
||||
<small className="absolute right-0 -top-5 text-red-500">{errors.username}</small>
|
||||
)}
|
||||
<Field
|
||||
name="username"
|
||||
@@ -82,9 +77,7 @@ const NPEDFields = () => {
|
||||
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>
|
||||
<small className="absolute right-0 -top-5 text-red-500">{errors.password}</small>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
type="button"
|
||||
@@ -97,9 +90,7 @@ const NPEDFields = () => {
|
||||
<FormGroup>
|
||||
<label htmlFor="clientId">Client ID</label>
|
||||
{touched.clientId && errors.clientId && (
|
||||
<small className="absolute right-0 -top-5 text-red-500">
|
||||
{errors.clientId}
|
||||
</small>
|
||||
<small className="absolute right-0 -top-5 text-red-500">{errors.clientId}</small>
|
||||
)}
|
||||
<Field
|
||||
name="clientId"
|
||||
@@ -109,7 +100,7 @@ const NPEDFields = () => {
|
||||
className="p-1.5 border border-gray-400 rounded-lg"
|
||||
/>
|
||||
</FormGroup>
|
||||
{!user?.propClientID?.value ? (
|
||||
{!state.npedUser?.propClientID?.value ? (
|
||||
<button
|
||||
type="submit"
|
||||
className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5 hover:cursor-pointer"
|
||||
|
||||
@@ -127,8 +127,8 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
|
||||
<div className="flex flex-col border-b border-gray-600 mb-4">
|
||||
<p className="text-gray-300">Hotlists</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-[90%] lg:gap-x-[15%] w-[50%]">
|
||||
{hotlistNames.map((hotlistName) => (
|
||||
<div className="items-center px-2.5 py-0.5 rounded-sm me-2 bg-amber-500 w-55 m-2">
|
||||
{hotlistNames.map((hotlistName, index) => (
|
||||
<div className="items-center px-2.5 py-0.5 rounded-sm me-2 bg-amber-500 w-55 m-2" key={index}>
|
||||
<p className="font-medium text-2xl break-all text-amber-800">
|
||||
{hotlistName ? hotlistName?.replace(/\.csv$/i, "") : "-"}
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ 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 { useIntegrationsContext } from "../../context/IntegrationsContext";
|
||||
import { useSoundContext } from "../../context/SoundContext";
|
||||
import Loading from "../UI/Loading";
|
||||
import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils";
|
||||
@@ -72,7 +72,9 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
||||
} = useSightingFeedContext();
|
||||
|
||||
const { dispatch } = useAlertHitContext();
|
||||
const { sessionStarted, setSessionList, sessionList, sessionPaused } = useNPEDContext();
|
||||
const { state: integrationState, dispatch: integrationDispatch } = useIntegrationsContext();
|
||||
const sessionStarted = integrationState.sessionStarted;
|
||||
const sessionPaused = integrationState.sessionPaused;
|
||||
|
||||
const processedRefs = useRef<Set<number | string>>(new Set());
|
||||
|
||||
@@ -99,10 +101,10 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
|
||||
if (!mostRecent) return;
|
||||
if (sessionPaused) return;
|
||||
const reducedMostRecent = reduceObject(mostRecent);
|
||||
setSessionList([...sessionList, reducedMostRecent]);
|
||||
integrationDispatch({ type: "ADD", payload: reducedMostRecent });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mostRecent, sessionStarted, setSessionList]);
|
||||
}, [mostRecent, sessionStarted]);
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(sighting: SightingType) => {
|
||||
|
||||
@@ -4,11 +4,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGear, faHome, faListCheck, faMaximize, faMinimize, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useState } from "react";
|
||||
import SoundBtn from "./SoundBtn";
|
||||
import { useNPEDContext } from "../../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../../context/IntegrationsContext";
|
||||
|
||||
export default function Header() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const { sessionStarted, sessionPaused } = useNPEDContext();
|
||||
const { state } = useIntegrationsContext();
|
||||
|
||||
const sessionStarted = state.sessionStarted;
|
||||
|
||||
const sessionPaused = state.sessionPaused;
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
|
||||
14
src/context/IntegrationsContext.ts
Normal file
14
src/context/IntegrationsContext.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createContext, useContext, type ActionDispatch } from "react";
|
||||
import type { NPEDACTION, NPEDSTATE } from "../types/types";
|
||||
|
||||
type IntegrationsValue = {
|
||||
state: NPEDSTATE;
|
||||
dispatch: ActionDispatch<[action: NPEDACTION]>;
|
||||
};
|
||||
|
||||
export const IntegrationsContext = createContext<IntegrationsValue | undefined>(undefined);
|
||||
export const useIntegrationsContext = () => {
|
||||
const ctx = useContext(IntegrationsContext);
|
||||
if (!ctx) throw new Error("useNPEDContext must be used within <IntegrationsProvider>");
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createContext, useContext, type SetStateAction } from "react";
|
||||
import type { DedupedSightings, NPEDUser, ReducedSightingType } from "../types/types";
|
||||
|
||||
type UserContextValue = {
|
||||
user: NPEDUser | null;
|
||||
setUser: React.Dispatch<SetStateAction<NPEDUser | null>>;
|
||||
sessionStarted: boolean;
|
||||
sessionPaused: boolean;
|
||||
setSessionPaused: React.Dispatch<SetStateAction<boolean>>;
|
||||
setSessionStarted: React.Dispatch<SetStateAction<boolean>>;
|
||||
sessionList: ReducedSightingType[];
|
||||
setSessionList: React.Dispatch<SetStateAction<ReducedSightingType[]>>;
|
||||
savedSightings: DedupedSightings;
|
||||
setSavedSightings: React.Dispatch<SetStateAction<DedupedSightings>>;
|
||||
};
|
||||
|
||||
export const NPEDUserContext = createContext<UserContextValue | undefined>(undefined);
|
||||
export const useNPEDContext = () => {
|
||||
const ctx = useContext(NPEDUserContext);
|
||||
if (!ctx) throw new Error("useNPEDContext must be used within <NPEDUserProvider>");
|
||||
return ctx;
|
||||
};
|
||||
36
src/context/providers/IntegrationsContextProvider.tsx
Normal file
36
src/context/providers/IntegrationsContextProvider.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useReducer, type ReactNode } from "react";
|
||||
import { IntegrationsContext } from "../IntegrationsContext";
|
||||
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
|
||||
import { initialState, reducer } from "../reducers/IntegrationsContextReducer";
|
||||
|
||||
type IntegrationsProviderType = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const IntegrationsProvider = ({ children }: IntegrationsProviderType) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { mutation } = useCameraBlackboard();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const result = await mutation.mutateAsync({
|
||||
operation: "VIEW",
|
||||
path: "sessionStats",
|
||||
});
|
||||
if (!result.result || typeof result.result === "string") return;
|
||||
|
||||
dispatch({ type: "UPDATE", payload: result?.result });
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
return (
|
||||
<IntegrationsContext.Provider
|
||||
value={{
|
||||
state,
|
||||
dispatch,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</IntegrationsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useEffect, useReducer, useState, type ReactNode } from "react";
|
||||
import type { DedupedSightings, NPEDUser, ReducedSightingType } from "../../types/types";
|
||||
import { NPEDUserContext } from "../NPEDUserContext";
|
||||
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
|
||||
import { initialState, reducer } from "../reducers/NPEDContextReducer";
|
||||
|
||||
type NPEDUserProviderType = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const NPEDUserProvider = ({ children }: NPEDUserProviderType) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { mutation } = useCameraBlackboard();
|
||||
const [user, setUser] = useState<NPEDUser | null>(null);
|
||||
const [sessionStarted, setSessionStarted] = useState(false);
|
||||
const [sessionList, setSessionList] = useState<ReducedSightingType[]>([]);
|
||||
const [sessionPaused, setSessionPaused] = useState(false);
|
||||
const [savedSightings, setSavedSightings] = useState<DedupedSightings | []>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const result = await mutation.mutateAsync({
|
||||
operation: "VIEW",
|
||||
path: "sessionStats",
|
||||
});
|
||||
if (!result.result) return;
|
||||
setSavedSightings(result?.result);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
console.log(savedSightings);
|
||||
return (
|
||||
<NPEDUserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
setUser,
|
||||
setSessionStarted,
|
||||
sessionStarted,
|
||||
sessionList,
|
||||
setSessionList,
|
||||
sessionPaused,
|
||||
setSessionPaused,
|
||||
savedSightings,
|
||||
setSavedSightings,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NPEDUserContext.Provider>
|
||||
);
|
||||
};
|
||||
46
src/context/reducers/IntegrationsContextReducer.ts
Normal file
46
src/context/reducers/IntegrationsContextReducer.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { NPEDACTION, NPEDSTATE } from "../../types/types";
|
||||
|
||||
export const initialState = {
|
||||
sessionStarted: false,
|
||||
sessionList: [],
|
||||
sessionPaused: false,
|
||||
savedSightings: [],
|
||||
npedUser: null,
|
||||
};
|
||||
|
||||
export function reducer(state: NPEDSTATE, action: NPEDACTION) {
|
||||
switch (action.type) {
|
||||
case "SESSIONSTART":
|
||||
return {
|
||||
...state,
|
||||
sessionStarted: action.payload,
|
||||
};
|
||||
case "LOGIN":
|
||||
return {
|
||||
...state,
|
||||
npedUser: action.payload,
|
||||
};
|
||||
case "LOGOUT":
|
||||
return {
|
||||
...state,
|
||||
npedUser: action.payload,
|
||||
};
|
||||
case "SESSIONPAUSE":
|
||||
return {
|
||||
...state,
|
||||
sessionPaused: action.payload,
|
||||
};
|
||||
case "ADD":
|
||||
return {
|
||||
...state,
|
||||
sessionList: [...state.sessionList, action.payload],
|
||||
};
|
||||
case "UPDATE":
|
||||
return {
|
||||
...state,
|
||||
sessionList: action.payload,
|
||||
};
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { NPEDACTION, NPEDSTATE } from "../../types/types";
|
||||
|
||||
export const initialState = {
|
||||
sessionStarted: false,
|
||||
sessionList: [],
|
||||
sessionPaused: false,
|
||||
savedSightings: [],
|
||||
};
|
||||
|
||||
export function reducer(state: NPEDSTATE, action: NPEDACTION) {
|
||||
switch (action.type) {
|
||||
case "SESSION":
|
||||
return {
|
||||
...state,
|
||||
sessionStarted: action.payload,
|
||||
};
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { NPEDFieldType } from "../types/types";
|
||||
import { useNPEDContext } from "../context/NPEDUserContext";
|
||||
import { useIntegrationsContext } from "../context/IntegrationsContext";
|
||||
import { useEffect } from "react";
|
||||
import { CAM_BASE } from "../utils/config";
|
||||
import { toast } from "sonner";
|
||||
@@ -42,8 +42,7 @@ async function signIn(loginDetails: NPEDFieldType) {
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!frontRes.ok || !rearRes.ok)
|
||||
throw new Error("Cannot reach NPED endpoint");
|
||||
if (!frontRes.ok || !rearRes.ok) throw new Error("Cannot reach NPED endpoint");
|
||||
|
||||
return {
|
||||
frontResponse: frontRes.json(),
|
||||
@@ -73,7 +72,7 @@ async function signOut() {
|
||||
}
|
||||
|
||||
export const useNPEDAuth = () => {
|
||||
const { setUser, user } = useNPEDContext();
|
||||
const { dispatch } = useIntegrationsContext();
|
||||
|
||||
const signInMutation = useMutation({
|
||||
mutationKey: ["NPEDSignin"],
|
||||
@@ -84,7 +83,8 @@ export const useNPEDAuth = () => {
|
||||
onSuccess: async (data) => {
|
||||
toast.dismiss();
|
||||
toast.success("Signed in successfully!");
|
||||
setUser(await data.frontResponse);
|
||||
|
||||
dispatch({ type: "LOGIN", payload: await data.frontResponse });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.dismiss();
|
||||
@@ -101,7 +101,7 @@ export const useNPEDAuth = () => {
|
||||
mutationFn: signOut,
|
||||
onSuccess: () => {
|
||||
toast.success("Signed out successfully");
|
||||
setUser(null);
|
||||
dispatch({ type: "LOGOUT", payload: null });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Sign-out failed: ${error.message}`);
|
||||
@@ -115,11 +115,11 @@ export const useNPEDAuth = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchdataQuery.isSuccess && fetchdataQuery.data) {
|
||||
setUser(fetchdataQuery.data);
|
||||
dispatch({ type: "LOGIN", payload: fetchdataQuery.data });
|
||||
} else {
|
||||
setUser(null);
|
||||
dispatch({ type: "LOGOUT", payload: null });
|
||||
}
|
||||
}, [fetchdataQuery.data, fetchdataQuery.isSuccess, setUser]);
|
||||
}, [dispatch, fetchdataQuery.data, fetchdataQuery.isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchdataQuery.isError) toast.error(fetchdataQuery.error.message);
|
||||
@@ -134,8 +134,6 @@ export const useNPEDAuth = () => {
|
||||
data: signInMutation.data,
|
||||
fetchdataQueryError: fetchdataQuery.error,
|
||||
fetchdataQueryLoading: fetchdataQuery.isLoading,
|
||||
user,
|
||||
setUser,
|
||||
signOut: signOutMutation.mutate,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -387,6 +387,7 @@ export type NPEDSTATE = {
|
||||
sessionList: ReducedSightingType[];
|
||||
sessionPaused: boolean;
|
||||
savedSightings: DedupedSightings;
|
||||
npedUser: NPEDUser;
|
||||
};
|
||||
|
||||
export type NPEDACTION = {
|
||||
|
||||
Reference in New Issue
Block a user