got to a good point with sighting modal, want to do cleanup
This commit is contained in:
@@ -6,10 +6,12 @@ import RearCamera from "./pages/RearCamera";
|
|||||||
import SystemSettings from "./pages/SystemSettings";
|
import SystemSettings from "./pages/SystemSettings";
|
||||||
import Session from "./pages/Session";
|
import Session from "./pages/Session";
|
||||||
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
|
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
|
||||||
|
import { AlertHitProvider } from "./context/providers/AlertHitProvider";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<NPEDUserProvider>
|
<NPEDUserProvider>
|
||||||
|
<AlertHitProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Container />}>
|
<Route path="/" element={<Container />}>
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
@@ -19,6 +21,7 @@ function App() {
|
|||||||
<Route path="session-settings" element={<Session />} />
|
<Route path="session-settings" element={<Session />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</AlertHitProvider>
|
||||||
</NPEDUserProvider>
|
</NPEDUserProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const SnapshotContainer = ({
|
|||||||
const { canvasRef, isError, isPending } = useGetOverviewSnapshot(side);
|
const { canvasRef, isError, isPending } = useGetOverviewSnapshot(side);
|
||||||
|
|
||||||
if (isError) return <>An error occurred</>;
|
if (isError) return <>An error occurred</>;
|
||||||
if (isPending) return <>loading...</>;
|
if (isPending) return <>Loading...</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full aspect-video">
|
<div className="relative w-full aspect-video">
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { Formik, Field, Form } from "formik";
|
import { Formik, Field, Form } from "formik";
|
||||||
import type {
|
import type {
|
||||||
|
CameraConfig,
|
||||||
CameraSettingErrorValues,
|
CameraSettingErrorValues,
|
||||||
CameraSettingValues,
|
CameraSettingValues,
|
||||||
} from "../../types/types";
|
} from "../../types/types";
|
||||||
|
|
||||||
const CameraSettingFields = ({ initialData, updateCameraConfig }) => {
|
type CameraSettingsProps = {
|
||||||
|
initialData: CameraConfig;
|
||||||
|
updateCameraConfig: (values: CameraSettingValues) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CameraSettingFields = ({
|
||||||
|
initialData,
|
||||||
|
updateCameraConfig,
|
||||||
|
}: CameraSettingsProps) => {
|
||||||
const initialValues: CameraSettingValues = {
|
const initialValues: CameraSettingValues = {
|
||||||
friendlyName: initialData?.propLEDDriverControlURI?.value,
|
friendlyName: initialData?.propLEDDriverControlURI?.value,
|
||||||
cameraAddress: "",
|
cameraAddress: "",
|
||||||
|
|||||||
48
src/components/HistoryList/AlertItem.tsx
Normal file
48
src/components/HistoryList/AlertItem.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { SightingType } from "../../types/types";
|
||||||
|
import NumberPlate from "../PlateStack/NumberPlate";
|
||||||
|
import SightingModal from "../SightingModal/SightingModal";
|
||||||
|
import InfoBar from "../SightingsWidget/InfoBar";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type AlertItemProps = {
|
||||||
|
item: SightingType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlertItem = ({ item }: AlertItemProps) => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const motionAway = (item?.motion ?? "").toUpperCase() === "AWAY";
|
||||||
|
const isNPEDHit = item?.metadata?.npedJSON?.status_code === 404;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InfoBar obj={item} />
|
||||||
|
<div
|
||||||
|
className=" flex flex-row p-4 border border-gray-400 rounded-lg items-center w-full mx-auto justify-between"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{isNPEDHit && <small className="text-red-500">NPED Hit</small>}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<small>MAKE: {item.make}</small>
|
||||||
|
<small>MODEL: {item.model}</small>
|
||||||
|
<small>COLOUR: {item.color}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NumberPlate vrm={item.vrm} motion={motionAway} />
|
||||||
|
</div>
|
||||||
|
<SightingModal
|
||||||
|
isSightingModalOpen={isModalOpen}
|
||||||
|
handleClose={closeModal}
|
||||||
|
sighting={item}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlertItem;
|
||||||
25
src/components/HistoryList/HistoryList.tsx
Normal file
25
src/components/HistoryList/HistoryList.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useAlertHitContext } from "../../context/AlertHitContext";
|
||||||
|
import Card from "../UI/Card";
|
||||||
|
import CardHeader from "../UI/CardHeader";
|
||||||
|
import AlertItem from "./AlertItem";
|
||||||
|
|
||||||
|
const HistoryList = () => {
|
||||||
|
const { state } = useAlertHitContext();
|
||||||
|
console.log(state);
|
||||||
|
return (
|
||||||
|
<Card className="h-100">
|
||||||
|
<CardHeader title="Alert History" />
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{state?.alertList?.length >= 0 ? (
|
||||||
|
state?.alertList?.map((alertItem, index) => (
|
||||||
|
<AlertItem key={index} item={alertItem} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p>No Search results</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryList;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import NumberPlate from "./NumberPlate";
|
|
||||||
|
|
||||||
import type { SightingType } from "../../types/types";
|
|
||||||
|
|
||||||
type SightingProps = {
|
|
||||||
sighting: SightingType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Sighting = ({ sighting }: SightingProps) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-700 flex flex-col md:flex-row m-1 items-center justify-between w-full rounded-md p-4 space-y-4">
|
|
||||||
<div className="flex flex-row m-1 items-center space-x-4">
|
|
||||||
<NumberPlate />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sighting;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import Card from "../UI/Card";
|
|
||||||
import SightingHeader from "./SightingHeader";
|
|
||||||
import Sighting from "./Sighting";
|
|
||||||
import { useLatestSighting } from "../../hooks/useLatestSighting";
|
|
||||||
|
|
||||||
type SightingProps = {
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Sightings = ({ title }: SightingProps) => {
|
|
||||||
const { data } = useLatestSighting();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="h-[10rem] md:h-[15rem] overflow-x-hidden">
|
|
||||||
<SightingHeader title={title} />
|
|
||||||
<Sighting sighting={data} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sightings;
|
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
import Card from "../UI/Card";
|
import Card from "../UI/Card";
|
||||||
import CardHeader from "../UI/CardHeader";
|
import CardHeader from "../UI/CardHeader";
|
||||||
import FormGroup from "../SettingForms/components/FormGroup";
|
import FormGroup from "../SettingForms/components/FormGroup";
|
||||||
|
import { useAlertHitContext } from "../../context/AlertHitContext";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const SessionCard = () => {
|
const SessionCard = () => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const { state, disptach } = useAlertHitContext();
|
||||||
|
|
||||||
|
console.log(state);
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title={"Hit Search"} />
|
<CardHeader title={"Hit Search"} />
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label htmlFor="VRM" className="font-medium whitespace-nowrap md:w-1/2 text-left">VRM (Min 2 letters)</label>
|
<label
|
||||||
|
htmlFor="VRM"
|
||||||
|
className="font-medium whitespace-nowrap md:w-1/2 text-left"
|
||||||
|
>
|
||||||
|
VRM (Min 2 letters)
|
||||||
|
</label>
|
||||||
<div className="flex-1 flex justify-end md:w-2/3">
|
<div className="flex-1 flex justify-end md:w-2/3">
|
||||||
<input
|
<input
|
||||||
id="VRMSelect"
|
id="VRMSelect"
|
||||||
@@ -17,13 +27,13 @@ const SessionCard = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||||
placeholder="Enter VRM"
|
placeholder="Enter VRM"
|
||||||
//onChange={e => setSntpServer(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<button
|
<button
|
||||||
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
|
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
|
||||||
//onClick={() => handleModemSave(apn, username, password, authType)}
|
onClick={() => disptach({ type: "SEARCH", payload: searchTerm })}
|
||||||
>
|
>
|
||||||
Search Hit list
|
Search Hit list
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Card from "../UI/Card";
|
|||||||
import CardHeader from "../UI/CardHeader";
|
import CardHeader from "../UI/CardHeader";
|
||||||
|
|
||||||
const SessionCard = () => {
|
const SessionCard = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title={"Session"} />
|
<CardHeader title={"Session"} />
|
||||||
@@ -13,12 +12,12 @@ const SessionCard = () => {
|
|||||||
>
|
>
|
||||||
Start Session
|
Start Session
|
||||||
</button>
|
</button>
|
||||||
<h2 className="text-white mb-2">Number of cars: </h2>
|
<h2 className="text-white mb-2">Number of Vehicles: </h2>
|
||||||
<h2 className="text-white mb-2">Cars without Tax: </h2>
|
<h2 className="text-white mb-2">Vehicles without Tax: </h2>
|
||||||
<h2 className="text-white mb-2">Cars without MOT: </h2>
|
<h2 className="text-white mb-2">Vehicles without MOT: </h2>
|
||||||
<h2 className="text-white mb-2">Cars with NPED Cat A: </h2>
|
<h2 className="text-white mb-2">Vehicles with NPED Cat A: </h2>
|
||||||
<h2 className="text-white mb-2">Cars with NPED Cat B: </h2>
|
<h2 className="text-white mb-2">Vehicles with NPED Cat B: </h2>
|
||||||
<h2 className="text-white mb-2">Cars with NPED Cat C: </h2>
|
<h2 className="text-white mb-2">Vehicles with NPED Cat C: </h2>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const ValuesComponent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BearerTypeFields = () => {
|
const BearerTypeFields = () => {
|
||||||
const { values } = useFormikContext();
|
useFormikContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ const ModemCard = () => {
|
|||||||
const [authType, setAuthType] = useState("PAP");
|
const [authType, setAuthType] = useState("PAP");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// TODO: Add switch for Auto vs Manual settings
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader title={"Modem"} />
|
<CardHeader title={"Modem"} />
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label htmlFor="apn" className="font-medium whitespace-nowrap md:w-2/3">APN</label>
|
<label
|
||||||
|
htmlFor="apn"
|
||||||
|
className="font-medium whitespace-nowrap md:w-2/3"
|
||||||
|
>
|
||||||
|
APN
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="apn"
|
id="apn"
|
||||||
name="apn"
|
name="apn"
|
||||||
@@ -22,11 +28,16 @@ const ModemCard = () => {
|
|||||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||||
placeholder="Enter APN"
|
placeholder="Enter APN"
|
||||||
value={apn}
|
value={apn}
|
||||||
onChange={e => setApn(e.target.value)}
|
onChange={(e) => setApn(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label htmlFor="modemUsername" className="font-medium whitespace-nowrap md:w-2/3">Username</label>
|
<label
|
||||||
|
htmlFor="modemUsername"
|
||||||
|
className="font-medium whitespace-nowrap md:w-2/3"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="modemUsername"
|
id="modemUsername"
|
||||||
name="modemUsername"
|
name="modemUsername"
|
||||||
@@ -34,11 +45,16 @@ const ModemCard = () => {
|
|||||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||||
placeholder="Enter Username"
|
placeholder="Enter Username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={e => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label htmlFor="modemPassword" className="font-medium whitespace-nowrap md:w-2/3">Password</label>
|
<label
|
||||||
|
htmlFor="modemPassword"
|
||||||
|
className="font-medium whitespace-nowrap md:w-2/3"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="modemPassword"
|
id="modemPassword"
|
||||||
name="modemPassword"
|
name="modemPassword"
|
||||||
@@ -46,17 +62,22 @@ const ModemCard = () => {
|
|||||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||||
placeholder="Enter Password"
|
placeholder="Enter Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label htmlFor="authType" className="font-medium whitespace-nowrap md:w-2/3">Authentication Type</label>
|
<label
|
||||||
|
htmlFor="authType"
|
||||||
|
className="font-medium whitespace-nowrap md:w-2/3"
|
||||||
|
>
|
||||||
|
Authentication Type
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
id="authType"
|
id="authType"
|
||||||
name="authType"
|
name="authType"
|
||||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 md:w-2/3"
|
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 md:w-2/3"
|
||||||
value={authType}
|
value={authType}
|
||||||
onChange={e => setAuthType(e.target.value)}
|
onChange={(e) => setAuthType(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="PAP">PAP</option>
|
<option value="PAP">PAP</option>
|
||||||
<option value="CHAP">CHAP</option>
|
<option value="CHAP">CHAP</option>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||||
import type { SightingType } from "../../types/types";
|
import type { SightingType } from "../../types/types";
|
||||||
import NumberPlate from "../PlateStack/NumberPlate";
|
import NumberPlate from "../PlateStack/NumberPlate";
|
||||||
import ModalComponent from "../UI/ModalComponent";
|
import ModalComponent from "../UI/ModalComponent";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
type SightingModalProps = {
|
type SightingModalProps = {
|
||||||
isSightingModalOpen: boolean;
|
isSightingModalOpen: boolean;
|
||||||
@@ -14,34 +16,103 @@ const SightingModal = ({
|
|||||||
sighting,
|
sighting,
|
||||||
}: SightingModalProps) => {
|
}: SightingModalProps) => {
|
||||||
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
|
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
|
||||||
console.log(sighting);
|
|
||||||
return (
|
return (
|
||||||
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
|
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
|
||||||
<div>
|
<div className="max-w-screen-lg mx-auto p-4">
|
||||||
<h2>Sighting Details</h2>
|
<div className="border-b border-gray-600 mb-4">
|
||||||
|
<h2 className="text-lg md:text-xl font-semibold">Sighting Details</h2>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleClose}>close</button>
|
|
||||||
<div>
|
<div className="flex flex-col gap-6 md:flex-row">
|
||||||
<div>
|
<div className="flex-1 flex flex-col gap-4">
|
||||||
|
<div className="flex justify-start md:justify-start">
|
||||||
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
|
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<img
|
<img
|
||||||
src={sighting?.overviewUrl}
|
src={sighting?.overviewUrl}
|
||||||
alt="overview patch"
|
alt="overview patch"
|
||||||
className="h-[50%] w-[50%]"
|
className="w-full h-56 sm:h-72 md:h-96 rounded-lg object-cover border border-gray-700"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={sighting?.plateUrlColour}
|
||||||
|
alt="plate patch"
|
||||||
|
className="h-16 w-auto object-contain rounded-md border border-gray-700"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<img src={sighting?.plateUrlColour} alt="plate patch" height={48} />
|
|
||||||
<img
|
<img
|
||||||
src={sighting?.plateUrlInfrared}
|
src={sighting?.plateUrlInfrared}
|
||||||
height={48}
|
|
||||||
alt="infrared patch"
|
alt="infrared patch"
|
||||||
className={"opacity-60"}
|
className="h-16 w-auto object-contain rounded-md border border-gray-700 opacity-70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside className="md:w-80 lg:w-96 bg-gray-800/70 text-white rounded-xl p-4 border border-gray-700">
|
||||||
|
<h3 className="text-base md:text-lg font-semibold pb-2 border-b border-gray-700">
|
||||||
|
Vehicle Info
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<dl className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-300">VRM</dt>
|
||||||
|
<dd className="font-medium break-all">
|
||||||
|
{sighting?.vrm ?? "-"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-300">Motion</dt>
|
||||||
|
<dd className="font-medium">{sighting?.motion ?? "-"}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-300">Seen Count</dt>
|
||||||
|
<dd className="font-medium">{sighting?.seenCount ?? "-"}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-300">Time</dt>
|
||||||
|
<dd className="font-medium">{sighting?.timeStamp ?? "-"}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<dt className="text-gray-300">Location</dt>
|
||||||
|
<dd className="font-medium truncate">
|
||||||
|
{sighting?.locationName ?? "-"}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-300">Make</dt>
|
||||||
|
<dd className="font-medium">{sighting?.make ?? "-"}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-300">Model</dt>
|
||||||
|
<dd className="font-medium">{sighting?.model ?? "-"}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<dt className="text-gray-300">Colour</dt>
|
||||||
|
<dd className="font-medium">{sighting?.color ?? "-"}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col-reverse gap-3 md:flex-row md:justify-center">
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-lg px-5 py-2.5 bg-red-600 text-white hover:bg-red-700 w-full md:w-auto"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faX} />
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-lg px-5 py-2.5 bg-green-600 text-white hover:bg-green-700 w-full md:w-auto"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
Acknowledge
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
32
src/components/SightingsWidget/InfoBar.tsx
Normal file
32
src/components/SightingsWidget/InfoBar.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { SightingType, SightingWidgetType } from "../../types/types";
|
||||||
|
import { capitalize, formatAge } from "../../utils/utils";
|
||||||
|
|
||||||
|
type InfoBarprops = {
|
||||||
|
obj: SightingWidgetType | SightingType;
|
||||||
|
};
|
||||||
|
const InfoBar = ({ obj }: InfoBarprops) => {
|
||||||
|
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 404;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded justify-between">
|
||||||
|
<div className="flex items-center gap-3 text-xs">
|
||||||
|
{" "}
|
||||||
|
<div className="min-w-14">CH: {obj ? obj.charHeight : "—"}</div>
|
||||||
|
<div className="min-w-14">Seen: {obj ? obj.seenCount : "—"}</div>
|
||||||
|
<div className="min-w-20">{obj ? capitalize(obj.motion) : "—"}</div>
|
||||||
|
<div className="min-w-14 opacity-80">
|
||||||
|
{obj ? formatAge(obj.timeStampMillis) : "—"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-14 opacity-80 ">
|
||||||
|
{isNPEDHit ? (
|
||||||
|
<span className="text-red-500 font-semibold">NPED HIT</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoBar;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import type { SightingType, SightingWidgetType } from "../../types/types";
|
import type { SightingType, SightingWidgetType } from "../../types/types";
|
||||||
import { BLANK_IMG, capitalize, formatAge } from "../../utils/utils";
|
import { BLANK_IMG } from "../../utils/utils";
|
||||||
import NumberPlate from "../PlateStack/NumberPlate";
|
import NumberPlate from "../PlateStack/NumberPlate";
|
||||||
import Card from "../UI/Card";
|
import Card from "../UI/Card";
|
||||||
import CardHeader from "../UI/CardHeader";
|
import CardHeader from "../UI/CardHeader";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useSightingFeedContext } from "../../context/SightingFeedContext";
|
import { useSightingFeedContext } from "../../context/SightingFeedContext";
|
||||||
import SightingModal from "../SightingModal/SightingModal";
|
import SightingModal from "../SightingModal/SightingModal";
|
||||||
|
import { useAlertHitContext } from "../../context/AlertHitContext";
|
||||||
|
import InfoBar from "./InfoBar";
|
||||||
|
|
||||||
function useNow(tickMs = 1000) {
|
function useNow(tickMs = 1000) {
|
||||||
const [, setNow] = useState(() => Date.now());
|
const [, setNow] = useState(() => Date.now());
|
||||||
@@ -42,6 +44,8 @@ export default function SightingHistoryWidget({
|
|||||||
selectedSighting,
|
selectedSighting,
|
||||||
} = useSightingFeedContext();
|
} = useSightingFeedContext();
|
||||||
|
|
||||||
|
const { disptach } = useAlertHitContext();
|
||||||
|
|
||||||
const onRowClick = useCallback(
|
const onRowClick = useCallback(
|
||||||
(sighting: SightingType | SightingWidgetType) => {
|
(sighting: SightingType | SightingWidgetType) => {
|
||||||
if (!sighting) return;
|
if (!sighting) return;
|
||||||
@@ -54,6 +58,20 @@ export default function SightingHistoryWidget({
|
|||||||
() => sightings?.filter(Boolean) as SightingWidgetType[],
|
() => sightings?.filter(Boolean) as SightingWidgetType[],
|
||||||
[sightings]
|
[sightings]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rows?.forEach((obj) => {
|
||||||
|
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 404;
|
||||||
|
|
||||||
|
if (isNPEDHit) {
|
||||||
|
disptach({
|
||||||
|
type: "ADD",
|
||||||
|
payload: obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [rows, disptach]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSightingModalOpen(false);
|
setSightingModalOpen(false);
|
||||||
};
|
};
|
||||||
@@ -69,6 +87,7 @@ export default function SightingHistoryWidget({
|
|||||||
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
|
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
|
||||||
const primaryIsColour = obj?.srcCam === 1;
|
const primaryIsColour = obj?.srcCam === 1;
|
||||||
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
|
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -76,40 +95,15 @@ export default function SightingHistoryWidget({
|
|||||||
onClick={() => onRowClick(obj)}
|
onClick={() => onRowClick(obj)}
|
||||||
>
|
>
|
||||||
{/* Info bar */}
|
{/* Info bar */}
|
||||||
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded justify-between">
|
<InfoBar obj={obj} />
|
||||||
<div className="flex items-center gap-3 text-xs">
|
|
||||||
{" "}
|
|
||||||
<div className="min-w-14">
|
|
||||||
CH: {obj ? obj.charHeight : "—"}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-14">
|
|
||||||
Seen: {obj ? obj.seenCount : "—"}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-20">
|
|
||||||
{obj ? capitalize(obj.motion) : "—"}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-14 opacity-80">
|
|
||||||
{obj ? formatAge(obj.timeStampMillis) : "—"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-14 opacity-80 ">
|
|
||||||
{isNPEDHit ? (
|
|
||||||
<span className="text-red-500 font-semibold">
|
|
||||||
NPED HIT
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Patch row */}
|
{/* Patch row */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-3 mt-2
|
className={`flex items-center gap-3 mt-2 justify-between
|
||||||
${isNPEDHit ? "border border-red-600" : ""}
|
${isNPEDHit ? "border border-red-600" : ""}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
{obj?.plateUrlInfrared && (
|
||||||
<div
|
<div
|
||||||
className={`border p-1 ${
|
className={`border p-1 ${
|
||||||
primaryIsColour ? "" : "ring-2 ring-lime-400"
|
primaryIsColour ? "" : "ring-2 ring-lime-400"
|
||||||
@@ -122,6 +116,8 @@ export default function SightingHistoryWidget({
|
|||||||
className={!primaryIsColour ? "" : "opacity-60"}
|
className={!primaryIsColour ? "" : "opacity-60"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`border p-1 ${
|
className={`border p-1 ${
|
||||||
primaryIsColour ? "ring-2 ring-lime-400" : ""
|
primaryIsColour ? "ring-2 ring-lime-400" : ""
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const ModalComponent = ({
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onRequestClose={close}
|
onRequestClose={close}
|
||||||
className="bg-[#1e2a38] p-6 rounded-lg shadow-lg max-w-[65%] mx-auto mt-20 w-full h-[75%] z-100"
|
className="bg-[#1e2a38] p-6 rounded-lg shadow-lg max-w-[90%] mx-auto mt-20 md:w-[70%] md:h-[80%] z-100"
|
||||||
overlayClassName="fixed inset-0 bg-[#1e2a38]/70 flex justify-center items-start z-100"
|
overlayClassName="fixed inset-0 bg-[#1e2a38]/70 flex justify-center items-start z-100"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
21
src/context/AlertHitContext.ts
Normal file
21
src/context/AlertHitContext.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import type { AlertState, AlertPayload, ActionType } from "../types/types";
|
||||||
|
|
||||||
|
type AlertHitContextValueType = {
|
||||||
|
state: AlertState;
|
||||||
|
action: AlertPayload;
|
||||||
|
disptach: (action: ActionType) => AlertState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AlertHitContext = createContext<
|
||||||
|
AlertHitContextValueType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export const useAlertHitContext = () => {
|
||||||
|
const ctx = useContext(AlertHitContext);
|
||||||
|
if (!ctx)
|
||||||
|
throw new Error("useAlertHitContext must be used within <AlertHitContext>");
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlertHitContext;
|
||||||
19
src/context/providers/AlertHitProvider.tsx
Normal file
19
src/context/providers/AlertHitProvider.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useReducer, type ReactNode } from "react";
|
||||||
|
import AlertHitContext from "../AlertHitContext";
|
||||||
|
import { reducer, initalState } from "../reducers/AlertReducers";
|
||||||
|
|
||||||
|
type AlertHitProviderTypeProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AlertHitProvider = ({ children }: AlertHitProviderTypeProps) => {
|
||||||
|
const [state, disptach] = useReducer(reducer, initalState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertHitContext.Provider value={{ state, disptach }}>
|
||||||
|
{children}
|
||||||
|
</AlertHitContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlertHitProvider;
|
||||||
46
src/context/reducers/AlertReducers.ts
Normal file
46
src/context/reducers/AlertReducers.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { AlertPayload, AlertState } from "../../types/types";
|
||||||
|
|
||||||
|
export const initalState = {
|
||||||
|
alertList: [],
|
||||||
|
allAlerts: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function reducer(state: AlertState, action: AlertPayload) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD": {
|
||||||
|
const alreadyExists = state.allAlerts.some(
|
||||||
|
(alertItem) => alertItem.vrm === action.payload.vrm
|
||||||
|
);
|
||||||
|
if (alreadyExists) {
|
||||||
|
return { ...state };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertList: [...state.allAlerts, action.payload],
|
||||||
|
allAlerts: [...state.allAlerts, action.payload],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "SEARCH": {
|
||||||
|
if (action.payload && typeof action.payload === "string") {
|
||||||
|
const searchTerm = action.payload.toLowerCase();
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertList: state.alertList.filter((alertItem) =>
|
||||||
|
alertItem.vrm.toLowerCase().includes(searchTerm)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(state);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertList: state.allAlerts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// Used to fetch and load the configs for the camera side
|
|
||||||
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -14,7 +12,7 @@ const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateCamerasideConfig = async (data: {
|
const updateCamerasideConfig = async (data: {
|
||||||
id: string;
|
id: string | number;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
}) => {
|
}) => {
|
||||||
const updateUrl = `${base_url}/update-config?id=${data.id}`;
|
const updateUrl = `${base_url}/update-config?id=${data.id}`;
|
||||||
@@ -48,6 +46,7 @@ export const useFetchCameraConfig = (cameraSide: string) => {
|
|||||||
onSuccess: () => toast("Settings Successfully saved"),
|
onSuccess: () => toast("Settings Successfully saved"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(fetchedConfigQuery.data);
|
||||||
return {
|
return {
|
||||||
data: fetchedConfigQuery.data,
|
data: fetchedConfigQuery.data,
|
||||||
isPending: fetchedConfigQuery.isPending,
|
isPending: fetchedConfigQuery.isPending,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ const apiUrl = import.meta.env.VITE_BASEURL;
|
|||||||
|
|
||||||
async function fetchSnapshot(cameraSide: string) {
|
async function fetchSnapshot(cameraSide: string) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://100.116.253.81/Colour-preview`
|
// `http://100.116.253.81/Colour-preview`
|
||||||
// `${apiUrl}/${cameraSide}-preview`
|
`${apiUrl}/${cameraSide}-preview`
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Cannot reach endpoint");
|
throw new Error("Cannot reach endpoint");
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import HistoryList from "../components/HistoryList/HistoryList.tsx";
|
||||||
import HitSearchCard from "../components/SessionForm/HitSearchCard.tsx";
|
import HitSearchCard from "../components/SessionForm/HitSearchCard.tsx";
|
||||||
import SessionCard from "../components/SessionForm/SessionCard.tsx";
|
import SessionCard from "../components/SessionForm/SessionCard.tsx";
|
||||||
|
import { useAlertHitContext } from "../context/AlertHitContext.ts";
|
||||||
|
|
||||||
const Session = () => {
|
const Session = () => {
|
||||||
|
useAlertHitContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full">
|
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full">
|
||||||
<HitSearchCard />
|
<HitSearchCard />
|
||||||
<SessionCard />
|
<SessionCard />
|
||||||
|
<div className="col-span-2">
|
||||||
|
<HistoryList />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Toaster } from "sonner";
|
|||||||
import { useNPEDAuth } from "../hooks/useNPEDAuth";
|
import { useNPEDAuth } from "../hooks/useNPEDAuth";
|
||||||
|
|
||||||
const SystemSettings = () => {
|
const SystemSettings = () => {
|
||||||
const { user } = useNPEDAuth();
|
useNPEDAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type SightingType = {
|
|||||||
overviewSize: string;
|
overviewSize: string;
|
||||||
radarSpeed: string;
|
radarSpeed: string;
|
||||||
trackSpeed: string;
|
trackSpeed: string;
|
||||||
|
metadata?: Metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CameraSettingValues = {
|
export type CameraSettingValues = {
|
||||||
@@ -34,7 +35,7 @@ export type CameraSettingValues = {
|
|||||||
cameraAddress: string;
|
cameraAddress: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
password: string;
|
password: string;
|
||||||
id: number;
|
id: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CameraSettingErrorValues = Partial<
|
export type CameraSettingErrorValues = Partial<
|
||||||
@@ -176,3 +177,85 @@ export type SystemValuesErrors = {
|
|||||||
timeZone?: string | undefined;
|
timeZone?: string | undefined;
|
||||||
softwareUpdate?: File | null;
|
softwareUpdate?: File | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AlertState = {
|
||||||
|
alertList: SightingType[];
|
||||||
|
allAlerts: SightingType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AlertPayload = {
|
||||||
|
payload: SightingType | string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActionType = {
|
||||||
|
payload: SightingType | string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CameraConfig = {
|
||||||
|
id: string;
|
||||||
|
configHash: string;
|
||||||
|
propURI: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propMonochrome: {
|
||||||
|
value: string; // "true" or "false" as string
|
||||||
|
datatype: "boolean";
|
||||||
|
};
|
||||||
|
propControlProtocol: {
|
||||||
|
value: "NONE" | "VISCA_KTNC" | "VISCA_WONWOO" | "DUMMY";
|
||||||
|
datatype: "mav.cameracontrol.CameraController$CameraControlProtocol";
|
||||||
|
accepted: "[NONE, VISCA_KTNC, VISCA_WONWOO, DUMMY]";
|
||||||
|
};
|
||||||
|
propAllowControlProtocolInference: {
|
||||||
|
value: string;
|
||||||
|
datatype: "boolean";
|
||||||
|
};
|
||||||
|
propCameraControlDeviceURI: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propCameraControllerBaudRate: {
|
||||||
|
value: string; // "115200" as string
|
||||||
|
datatype: "int";
|
||||||
|
};
|
||||||
|
propLEDControllerType: {
|
||||||
|
value: "None" | "Serial" | "I2C";
|
||||||
|
datatype: "mav.flexi.modules.originators.video.LEDController$InterfaceType";
|
||||||
|
accepted: "[None, Serial, I2C]";
|
||||||
|
};
|
||||||
|
propLEDDriverControlURI: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propSourceIdentifier: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propI2CLEDDriverSafeHex: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propI2CLEDDriverLowHex: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propI2CLEDDriverMidHex: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propI2CLEDDriverHighHex: {
|
||||||
|
value: string;
|
||||||
|
datatype: "java.lang.String";
|
||||||
|
};
|
||||||
|
propRecordCameraControllerLogs: {
|
||||||
|
value: string;
|
||||||
|
datatype: "boolean";
|
||||||
|
};
|
||||||
|
propVideoFeedWatchdogEnabled: {
|
||||||
|
value: string;
|
||||||
|
datatype: "boolean";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user