got to a good point with sighting modal, want to do cleanup

This commit is contained in:
2025-09-16 11:07:35 +01:00
parent c414342515
commit c506c395e6
25 changed files with 490 additions and 141 deletions

View File

@@ -13,7 +13,7 @@ export const SnapshotContainer = ({
const { canvasRef, isError, isPending } = useGetOverviewSnapshot(side);
if (isError) return <>An error occurred</>;
if (isPending) return <>loading...</>;
if (isPending) return <>Loading...</>;
return (
<div className="relative w-full aspect-video">

View File

@@ -1,10 +1,19 @@
import { Formik, Field, Form } from "formik";
import type {
CameraConfig,
CameraSettingErrorValues,
CameraSettingValues,
} from "../../types/types";
const CameraSettingFields = ({ initialData, updateCameraConfig }) => {
type CameraSettingsProps = {
initialData: CameraConfig;
updateCameraConfig: (values: CameraSettingValues) => void;
};
const CameraSettingFields = ({
initialData,
updateCameraConfig,
}: CameraSettingsProps) => {
const initialValues: CameraSettingValues = {
friendlyName: initialData?.propLEDDriverControlURI?.value,
cameraAddress: "",

View 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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,15 +1,25 @@
import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
import FormGroup from "../SettingForms/components/FormGroup";
import { useAlertHitContext } from "../../context/AlertHitContext";
import { useState } from "react";
const SessionCard = () => {
const [searchTerm, setSearchTerm] = useState("");
const { state, disptach } = useAlertHitContext();
console.log(state);
return (
<Card>
<CardHeader title={"Hit Search"} />
<div className="flex flex-col gap-4">
<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">
<input
id="VRMSelect"
@@ -17,13 +27,13 @@ const SessionCard = () => {
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter VRM"
//onChange={e => setSntpServer(e.target.value)}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</FormGroup>
<button
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
</button>

View File

@@ -2,7 +2,6 @@ import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
const SessionCard = () => {
return (
<Card>
<CardHeader title={"Session"} />
@@ -13,12 +12,12 @@ const SessionCard = () => {
>
Start Session
</button>
<h2 className="text-white mb-2">Number of cars: </h2>
<h2 className="text-white mb-2">Cars without Tax: </h2>
<h2 className="text-white mb-2">Cars without MOT: </h2>
<h2 className="text-white mb-2">Cars with NPED Cat A: </h2>
<h2 className="text-white mb-2">Cars with NPED Cat B: </h2>
<h2 className="text-white mb-2">Cars with NPED Cat C: </h2>
<h2 className="text-white mb-2">Number of Vehicles: </h2>
<h2 className="text-white mb-2">Vehicles without Tax: </h2>
<h2 className="text-white mb-2">Vehicles without MOT: </h2>
<h2 className="text-white mb-2">Vehicles with NPED Cat A: </h2>
<h2 className="text-white mb-2">Vehicles with NPED Cat B: </h2>
<h2 className="text-white mb-2">Vehicles with NPED Cat C: </h2>
</div>
</Card>
);

View File

@@ -7,7 +7,7 @@ export const ValuesComponent = () => {
};
const BearerTypeFields = () => {
const { values } = useFormikContext();
useFormikContext();
return (
<div className="flex flex-col space-y-4">

View File

@@ -10,11 +10,17 @@ const ModemCard = () => {
const [authType, setAuthType] = useState("PAP");
return (
// TODO: Add switch for Auto vs Manual settings
<Card>
<CardHeader title={"Modem"} />
<div className="flex flex-col gap-4">
<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
id="apn"
name="apn"
@@ -22,11 +28,16 @@ const ModemCard = () => {
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
placeholder="Enter APN"
value={apn}
onChange={e => setApn(e.target.value)}
onChange={(e) => setApn(e.target.value)}
/>
</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
id="modemUsername"
name="modemUsername"
@@ -34,11 +45,16 @@ const ModemCard = () => {
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
placeholder="Enter Username"
value={username}
onChange={e => setUsername(e.target.value)}
onChange={(e) => setUsername(e.target.value)}
/>
</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
id="modemPassword"
name="modemPassword"
@@ -46,17 +62,22 @@ const ModemCard = () => {
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
placeholder="Enter Password"
value={password}
onChange={e => setPassword(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
/>
</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
id="authType"
name="authType"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 md:w-2/3"
value={authType}
onChange={e => setAuthType(e.target.value)}
onChange={(e) => setAuthType(e.target.value)}
>
<option value="PAP">PAP</option>
<option value="CHAP">CHAP</option>

View File

@@ -1,6 +1,8 @@
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import type { SightingType } from "../../types/types";
import NumberPlate from "../PlateStack/NumberPlate";
import ModalComponent from "../UI/ModalComponent";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type SightingModalProps = {
isSightingModalOpen: boolean;
@@ -14,32 +16,101 @@ const SightingModal = ({
sighting,
}: SightingModalProps) => {
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
console.log(sighting);
return (
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
<div>
<h2>Sighting Details</h2>
</div>
<button onClick={handleClose}>close</button>
<div>
<div>
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
<div className="max-w-screen-lg mx-auto p-4">
<div className="border-b border-gray-600 mb-4">
<h2 className="text-lg md:text-xl font-semibold">Sighting Details</h2>
</div>
<div>
<img
src={sighting?.overviewUrl}
alt="overview patch"
className="h-[50%] w-[50%]"
/>
<div className="flex flex-col gap-6 md:flex-row">
<div className="flex-1 flex flex-col gap-4">
<div className="flex justify-start md:justify-start">
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
</div>
<img
src={sighting?.overviewUrl}
alt="overview patch"
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"
/>
<img
src={sighting?.plateUrlInfrared}
alt="infrared patch"
className="h-16 w-auto object-contain rounded-md border border-gray-700 opacity-70"
/>
</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>
<img src={sighting?.plateUrlColour} alt="plate patch" height={48} />
<img
src={sighting?.plateUrlInfrared}
height={48}
alt="infrared patch"
className={"opacity-60"}
/>
<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>

View 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;

View File

@@ -1,12 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from "react";
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 Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
import clsx from "clsx";
import { useSightingFeedContext } from "../../context/SightingFeedContext";
import SightingModal from "../SightingModal/SightingModal";
import { useAlertHitContext } from "../../context/AlertHitContext";
import InfoBar from "./InfoBar";
function useNow(tickMs = 1000) {
const [, setNow] = useState(() => Date.now());
@@ -42,6 +44,8 @@ export default function SightingHistoryWidget({
selectedSighting,
} = useSightingFeedContext();
const { disptach } = useAlertHitContext();
const onRowClick = useCallback(
(sighting: SightingType | SightingWidgetType) => {
if (!sighting) return;
@@ -54,6 +58,20 @@ export default function SightingHistoryWidget({
() => sightings?.filter(Boolean) as SightingWidgetType[],
[sightings]
);
useEffect(() => {
rows?.forEach((obj) => {
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 404;
if (isNPEDHit) {
disptach({
type: "ADD",
payload: obj,
});
}
});
}, [rows, disptach]);
const handleClose = () => {
setSightingModalOpen(false);
};
@@ -69,6 +87,7 @@ export default function SightingHistoryWidget({
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const primaryIsColour = obj?.srcCam === 1;
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
return (
<div
key={idx}
@@ -76,52 +95,29 @@ export default function SightingHistoryWidget({
onClick={() => onRowClick(obj)}
>
{/* Info bar */}
<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>
<InfoBar obj={obj} />
{/* Patch row */}
<div
className={`flex items-center gap-3 mt-2
className={`flex items-center gap-3 mt-2 justify-between
${isNPEDHit ? "border border-red-600" : ""}
`}
>
<div
className={`border p-1 ${
primaryIsColour ? "" : "ring-2 ring-lime-400"
} ${!obj ? "opacity-30" : ""}`}
>
<img
src={obj?.plateUrlInfrared || BLANK_IMG}
height={48}
alt="infrared patch"
className={!primaryIsColour ? "" : "opacity-60"}
/>
</div>
{obj?.plateUrlInfrared && (
<div
className={`border p-1 ${
primaryIsColour ? "" : "ring-2 ring-lime-400"
} ${!obj ? "opacity-30" : ""}`}
>
<img
src={obj?.plateUrlInfrared || BLANK_IMG}
height={48}
alt="infrared patch"
className={!primaryIsColour ? "" : "opacity-60"}
/>
</div>
)}
<div
className={`border p-1 ${
primaryIsColour ? "ring-2 ring-lime-400" : ""

View File

@@ -16,7 +16,7 @@ const ModalComponent = ({
<Modal
isOpen={isModalOpen}
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"
>
{children}