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

@@ -6,19 +6,22 @@ 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>
<Routes> <AlertHitProvider>
<Route path="/" element={<Container />}> <Routes>
<Route index element={<Dashboard />} /> <Route path="/" element={<Container />}>
<Route path="front-camera-settings" element={<FrontCamera />} /> <Route index element={<Dashboard />} />
<Route path="rear-camera-settings" element={<RearCamera />} /> <Route path="front-camera-settings" element={<FrontCamera />} />
<Route path="system-settings" element={<SystemSettings />} /> <Route path="rear-camera-settings" element={<RearCamera />} />
<Route path="session-settings" element={<Session />} /> <Route path="system-settings" element={<SystemSettings />} />
</Route> <Route path="session-settings" element={<Session />} />
</Routes> </Route>
</Routes>
</AlertHitProvider>
</NPEDUserProvider> </NPEDUserProvider>
); );
} }

View File

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

View File

@@ -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: "",

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

View File

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

View File

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

View File

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

View File

@@ -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,32 +16,101 @@ 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">
</div> <h2 className="text-lg md:text-xl font-semibold">Sighting Details</h2>
<button onClick={handleClose}>close</button>
<div>
<div>
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
</div> </div>
<div>
<img <div className="flex flex-col gap-6 md:flex-row">
src={sighting?.overviewUrl} <div className="flex-1 flex flex-col gap-4">
alt="overview patch" <div className="flex justify-start md:justify-start">
className="h-[50%] w-[50%]" <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>
<div>
<img src={sighting?.plateUrlColour} alt="plate patch" height={48} /> <div className="mt-6 flex flex-col-reverse gap-3 md:flex-row md:justify-center">
<img <button
src={sighting?.plateUrlInfrared} 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"
height={48} onClick={handleClose}
alt="infrared patch" >
className={"opacity-60"} <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>
</div> </div>
</ModalComponent> </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 { 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,52 +95,29 @@ 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" : ""}
`} `}
> >
<div {obj?.plateUrlInfrared && (
className={`border p-1 ${ <div
primaryIsColour ? "" : "ring-2 ring-lime-400" className={`border p-1 ${
} ${!obj ? "opacity-30" : ""}`} primaryIsColour ? "" : "ring-2 ring-lime-400"
> } ${!obj ? "opacity-30" : ""}`}
<img >
src={obj?.plateUrlInfrared || BLANK_IMG} <img
height={48} src={obj?.plateUrlInfrared || BLANK_IMG}
alt="infrared patch" height={48}
className={!primaryIsColour ? "" : "opacity-60"} alt="infrared patch"
/> 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" : ""

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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