code fixes and adding modal

This commit is contained in:
2025-09-12 08:21:52 +01:00
parent fae17b88a4
commit d03f73f751
24 changed files with 524 additions and 303 deletions

View File

@@ -11,6 +11,7 @@ The selected sighting in the sighting stack seems a tad buggy. Sometimes multipl
Can the selected sighting be shown in full detail. How this will look is still up for debate. Either as a pop up card as in AiQ Flexi, or in the OVerview card?? Can the selected sighting be shown in full detail. How this will look is still up for debate. Either as a pop up card as in AiQ Flexi, or in the OVerview card??
How do you know if the time has sync? Make UTC red if not sync. How do you know if the time has sync? Make UTC red if not sync.
Can the relative aspect ratio in SightingOverview.tsx be the ratio of image pixel size of the image to best take advantage of the space? Can the relative aspect ratio in SightingOverview.tsx be the ratio of image pixel size of the image to best take advantage of the space?
obscure details on dashboard to a toggle
FYI: FYI:

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/MAV-Blue.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MAV | In Car System</title> <title>MAV | In Car System</title>
</head> </head>

18
public/MAV-Blue.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 231.27 52.63">
<defs>
<style>
.cls-1 {
fill: #20456f;
}
</style>
</defs>
<g id="Layer_2-2" data-name="Layer_2">
<g>
<g id="Layer_1-2">
<path class="cls-1" d="M150.57,0h-40.57c-7.53,0-13.64,6.11-13.64,13.64v38.99h13.64v-13.68h40.57v13.68h13.64V13.64c0-7.53-6.11-13.64-13.64-13.64ZM110,28.55v-12.59c0-1.72,1.39-3.11,3.11-3.11h34.34c1.72,0,3.11,1.39,3.11,3.11v12.59h-40.57,0ZM88.45,13.64v38.99h-13.64V15.96c0-1.72-1.39-3.11-3.11-3.11h-17.5c-1.72,0-3.11,1.39-3.11,3.11v36.67h-13.73V15.96c0-1.72-1.39-3.11-3.11-3.11h-17.49c-1.72,0-3.11,1.39-3.11,3.11v36.67H0V13.64C0,6.11,6.11,0,13.64,0h23.55c2.72,0,5.18,1.05,7.03,2.76,1.85-1.71,4.32-2.76,7.03-2.76h23.55c7.53,0,13.64,6.11,13.64,13.64h.01ZM193.88,52.63c-1.19,0-2.28-.68-2.8-1.75L166.25,0h13.16c1.19,0,2.28.68,2.8,1.75,0,0,12.25,25.11,16.55,33.92,4.3-8.81,16.55-33.92,16.55-33.92.53-1.07,1.61-1.75,2.8-1.75h13.16l-24.83,50.88c-.52,1.07-1.61,1.75-2.8,1.75h-9.78.02Z"/>
</g>
<path class="cls-1" d="M222.79,48.39c0-2.36,1.9-4.24,4.24-4.24s4.24,1.88,4.24,4.24-1.88,4.24-4.24,4.24-4.24-1.9-4.24-4.24ZM223.45,48.39c0,1.96,1.6,3.58,3.58,3.58s3.56-1.62,3.56-3.58-1.58-3.56-3.56-3.56-3.58,1.56-3.58,3.56ZM228.17,50.83l-1.26-1.92h-.8v1.92h-.72v-4.86h1.98c.9,0,1.62.58,1.62,1.48,0,1.08-.96,1.44-1.24,1.44l1.3,1.94h-.88ZM226.11,46.57v1.72h1.26c.5,0,.88-.34.88-.84,0-.54-.38-.88-.88-.88h-1.26Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -5,13 +5,13 @@ import type {
} from "../../types/types"; } from "../../types/types";
import { toast } from "sonner"; import { toast } from "sonner";
const CameraSettingFields = () => { const CameraSettingFields = ({ initialData, updateCameraConfig }) => {
const initialValues: CameraSettingValues = { const initialValues: CameraSettingValues = {
friendlyName: "", friendlyName: initialData?.propLEDDriverControlURI?.value,
cameraAddress: "", cameraAddress: "",
userName: "", userName: "",
password: "", password: "",
setupCamera: 1, id: initialData?.id,
}; };
const validateValues = (values: CameraSettingValues) => { const validateValues = (values: CameraSettingValues) => {
@@ -29,7 +29,7 @@ const CameraSettingFields = () => {
const handleSubmit = (values: CameraSettingValues) => { const handleSubmit = (values: CameraSettingValues) => {
// post values to endpoint // post values to endpoint
console.log(values); updateCameraConfig(values);
toast("Settings Saved"); toast("Settings Saved");
}; };

View File

@@ -1,15 +1,28 @@
import { useFetchCameraConfig } from "../../hooks/useCameraConfig";
import Card from "../UI/Card"; import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader"; import CardHeader from "../UI/CardHeader";
import CameraSettingFields from "./CameraSettingFields"; import CameraSettingFields from "./CameraSettingFields";
import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { faWrench } from "@fortawesome/free-solid-svg-icons";
const CameraSettings = ({ title }: { title: string }) => { const CameraSettings = ({ title, side }: { title: string; side: string }) => {
const { data, isError, isPending, updateCameraConfig } =
useFetchCameraConfig(side);
return ( return (
<Card> <Card>
{isError && <>Cannot Fetch camera config</>}
{isPending ? (
<>Loading</>
) : (
<div className="relative flex flex-col space-y-3 h-full"> <div className="relative flex flex-col space-y-3 h-full">
<CardHeader title={title} icon={faWrench} /> <CardHeader title={title} icon={faWrench} />
<CameraSettingFields /> <CameraSettingFields
initialData={data}
updateCameraConfig={updateCameraConfig}
/>
</div> </div>
)}
</Card> </Card>
); );
}; };

View File

@@ -1,12 +1,18 @@
export async function handleSystemSave(deviceName: string, sntpServer: string, sntpInterval: number, timeZone: string) { export async function handleSystemSave(
const payload = { // Build JSON deviceName: string,
sntpServer: string,
sntpInterval: number,
timeZone: string
) {
const payload = {
// Build JSON
id: "GLOBAL--Device", id: "GLOBAL--Device",
fields: [ fields: [
{ property: "propDeviceName", value: deviceName }, { property: "propDeviceName", value: deviceName },
{ property: "propSNTPServer", value: sntpServer }, { property: "propSNTPServer", value: sntpServer },
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) }, { property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
{ property: "propLocalTimeZone", value: timeZone } { property: "propLocalTimeZone", value: timeZone },
] ],
}; };
try { try {
@@ -14,14 +20,18 @@ export async function handleSystemSave(deviceName: string, sntpServer: string, s
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json" Accept: "application/json",
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload),
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text().catch(() => ""); const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`); throw new Error(
`HTTP ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`
);
} }
alert("System Settings Saved Successfully!"); alert("System Settings Saved Successfully!");
@@ -39,13 +49,17 @@ export async function handleSystemRecall() {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { "Accept": "application/json" }, headers: { Accept: "application/json" },
signal: controller.signal signal: controller.signal,
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text().catch(() => ""); const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`); throw new Error(
`HTTP ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`
);
} }
const data = await response.json(); const data = await response.json();
@@ -54,7 +68,7 @@ export async function handleSystemRecall() {
const sntpServer = data?.propSNTPServer?.value ?? null; const sntpServer = data?.propSNTPServer?.value ?? null;
const timeZone = data?.propLocalTimeZone?.value ?? null; const timeZone = data?.propLocalTimeZone?.value ?? null;
let sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value; const sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value;
let sntpInterval = let sntpInterval =
typeof sntpIntervalRaw === "number" typeof sntpIntervalRaw === "number"
? sntpIntervalRaw ? sntpIntervalRaw

View File

@@ -0,0 +1,32 @@
import NumberPlate from "../PlateStack/NumberPlate";
import ModalComponent from "../UI/ModalComponent";
type SightingModalProps = {
isSightingModalOpen: boolean;
handleClose: () => void;
};
const SightingModal = ({
isSightingModalOpen,
handleClose,
sighting,
}: SightingModalProps) => {
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
return (
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
<button onClick={handleClose}>close</button>
<div>
<h2>{sighting?.vrm}</h2>
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
<img
src={sighting?.plateUrlInfrared}
height={48}
alt="infrared patch"
className={"opacity-60"}
/>
</div>
</ModalComponent>
);
};
export default SightingModal;

View File

@@ -5,15 +5,15 @@ import { useOverviewOverlay } from "../../hooks/useOverviewOverlay";
import { useSightingFeedContext } from "../../context/SightingFeedContext"; import { useSightingFeedContext } from "../../context/SightingFeedContext";
import { useHiDPICanvas } from "../../hooks/useHiDPICanvas"; import { useHiDPICanvas } from "../../hooks/useHiDPICanvas";
import NavigationArrow from "../UI/NavigationArrow"; import NavigationArrow from "../UI/NavigationArrow";
import { useSwipeable } from "react-swipeable"; // import { useSwipeable } from "react-swipeable";
import { useNavigate } from "react-router"; // import { useNavigate } from "react-router";
const SightingOverview = () => { const SightingOverview = () => {
const navigate = useNavigate(); // const navigate = useNavigate();
const handlers = useSwipeable({ // const handlers = useSwipeable({
onSwipedRight: () => navigate("/front-camera-settings"), // onSwipedRight: () => navigate("/front-camera-settings"),
trackMouse: true, // trackMouse: true,
}); // });
const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0); const [overlayMode, setOverlayMode] = useState<0 | 1 | 2>(0);
const imgRef = useRef<HTMLImageElement | null>(null); const imgRef = useRef<HTMLImageElement | null>(null);
@@ -23,18 +23,19 @@ const SightingOverview = () => {
setOverlayMode((m) => ((m + 1) % 3) as 0 | 1 | 2); setOverlayMode((m) => ((m + 1) % 3) as 0 | 1 | 2);
}, []); }, []);
const { effectiveSelected, side, mostRecent, noSighting, isPending } = const { effectiveSelected, side, mostRecent } = useSightingFeedContext();
useSightingFeedContext();
useOverviewOverlay(mostRecent, overlayMode, imgRef, canvasRef); useOverviewOverlay(mostRecent, overlayMode, imgRef, canvasRef);
const { sync } = useHiDPICanvas(imgRef, canvasRef); const { sync } = useHiDPICanvas(imgRef, canvasRef);
if (noSighting || isPending) return <p>loading</p>; // if (noSighting || isPending) return <p>loading</p>;
return ( return (
<div className="mt-2 grid gap-3"> <div className="flex flex-col">
<div className="inline-block w-[90%] mx-auto" {...handlers}> <div className="grid gap-3">
<NavigationArrow side={side} /> <NavigationArrow side={side} />
<div className="inline-block w-full mx-auto">
<div className="relative aspect-[1280/800]"> <div className="relative aspect-[1280/800]">
<img <img
ref={imgRef} ref={imgRef}
@@ -44,7 +45,7 @@ const SightingOverview = () => {
}} }}
src={mostRecent?.overviewUrl || BLANK_IMG} src={mostRecent?.overviewUrl || BLANK_IMG}
alt="overview" alt="overview"
className="absolute inset-0 w-full h-full object-contain cursor-pointer z-10" className="absolute inset-0 w-full h-full object-contain cursor-pointer z-10 "
onClick={onOverviewClick} onClick={onOverviewClick}
style={{ style={{
display: mostRecent?.overviewUrl ? "block" : "none", display: mostRecent?.overviewUrl ? "block" : "none",
@@ -52,13 +53,10 @@ const SightingOverview = () => {
/> />
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="absolute inset-0 w-full h-full object-contain z-20 pointer-events-none" className="absolute inset-0 w-full h-full object-contain z-20 pointer-events-none "
/> />
</div> </div>
</div> </div>
<SightingWidgetDetails effectiveSelected={effectiveSelected} />
<div className="text-xs opacity-80"> <div className="text-xs opacity-80">
Overlay:{" "} Overlay:{" "}
{overlayMode === 0 {overlayMode === 0
@@ -69,6 +67,8 @@ const SightingOverview = () => {
(click image to toggle) (click image to toggle)
</div> </div>
</div> </div>
<SightingWidgetDetails effectiveSelected={effectiveSelected} />
</div>
); );
}; };

View File

@@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import type { SightingWidgetType } from "../../types/types"; import type { SightingType, SightingWidgetType } from "../../types/types";
import { BLANK_IMG, capitalize, formatAge } from "../../utils/utils"; import { BLANK_IMG, capitalize, formatAge } 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";
function useNow(tickMs = 1000) { function useNow(tickMs = 1000) {
const [, setNow] = useState(() => Date.now()); const [, setNow] = useState(() => Date.now());
@@ -29,21 +30,32 @@ export default function SightingHistoryWidget({
className, className,
}: SightingHistoryWidgetProps) { }: SightingHistoryWidgetProps) {
useNow(1000); useNow(1000);
const { sightings, selectedRef, setSelectedRef } = useSightingFeedContext();
const {
sightings,
setSelectedSighting,
setSightingModalOpen,
isSightingModalOpen,
selectedSighting,
} = useSightingFeedContext();
const onRowClick = useCallback( const onRowClick = useCallback(
(ref: number) => { (sighting: SightingType) => {
setSelectedRef(ref); if (!sighting) return;
setSightingModalOpen(!isSightingModalOpen);
setSelectedSighting(sighting);
}, },
[setSelectedRef] [isSightingModalOpen, setSelectedSighting, setSightingModalOpen]
); );
const rows = useMemo( const rows = useMemo(
() => sightings?.filter(Boolean) as SightingWidgetType[], () => sightings?.filter(Boolean) as SightingWidgetType[],
[sightings] [sightings]
); );
const handleClose = () => {
setSightingModalOpen(false);
};
return ( return (
<>
<Card className={clsx("overflow-y-auto h-100", className)}> <Card className={clsx("overflow-y-auto h-100", className)}>
<CardHeader title="Front Camera Sightings" /> <CardHeader title="Front Camera Sightings" />
<div className="flex flex-col gap-3 "> <div className="flex flex-col gap-3 ">
@@ -51,17 +63,15 @@ export default function SightingHistoryWidget({
<div className="flex flex-col"> <div className="flex flex-col">
{rows?.map((obj, idx) => { {rows?.map((obj, idx) => {
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 201; const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 201;
const isSelected = obj?.ref === selectedRef;
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 ?? "") === "";
console.log(obj);
return ( return (
<div <div
key={idx} key={idx}
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer ${ className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer`}
isSelected ? "ring-2 ring-blue-400" : "" onClick={() => onRowClick(obj)}
}`}
onClick={() => onRowClick(obj.ref)}
> >
{/* Info bar */} {/* Info bar */}
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded"> <div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
@@ -121,5 +131,11 @@ export default function SightingHistoryWidget({
</div> </div>
</div> </div>
</Card> </Card>
<SightingModal
isSightingModalOpen={isSightingModalOpen}
handleClose={handleClose}
sighting={selectedSighting}
/>
</>
); );
} }

View File

@@ -1,4 +1,5 @@
import type { SightingWidgetType } from "../../types/types"; import type { SightingWidgetType } from "../../types/types";
import { useState } from "react";
type SightingWidgetDetailsProps = { type SightingWidgetDetailsProps = {
effectiveSelected: SightingWidgetType | null; effectiveSelected: SightingWidgetType | null;
@@ -7,16 +8,19 @@ type SightingWidgetDetailsProps = {
const SightingWidgetDetails = ({ const SightingWidgetDetails = ({
effectiveSelected, effectiveSelected,
}: SightingWidgetDetailsProps) => { }: SightingWidgetDetailsProps) => {
const [advancedDetailsEnabled, setAdvancedDetailsEnabled] = useState(false);
const handleDetailsClick = () =>
setAdvancedDetailsEnabled(!advancedDetailsEnabled);
return ( return (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
<div> <div>
VRM:{" "} VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? "—"}</span> <span className="opacity-90">{effectiveSelected?.vrm ?? "—"}</span>
</div> </div>
<div>
Timestamp:{" "}
<span className="opacity-90">{effectiveSelected?.timeStamp ?? "—"}</span>
</div>
<div> <div>
Make:{" "} Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span> <span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
@@ -25,9 +29,24 @@ const SightingWidgetDetails = ({
Model:{" "} Model:{" "}
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span> <span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
</div> </div>
<div>
Colour:{" "}
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
</div>
<div className="col-span-4">
Timestamp:{" "}
<span className="opacity-90">
{effectiveSelected?.timeStamp ?? "—"}
</span>
</div>
{advancedDetailsEnabled && (
<>
{" "}
<div> <div>
Country:{" "} Country:{" "}
<span className="opacity-90">{effectiveSelected?.countryCode ?? "—"}</span> <span className="opacity-90">
{effectiveSelected?.countryCode ?? "—"}
</span>
</div> </div>
<div> <div>
Seen:{" "} Seen:{" "}
@@ -35,13 +54,11 @@ const SightingWidgetDetails = ({
{effectiveSelected?.seenCount ?? "—"} {effectiveSelected?.seenCount ?? "—"}
</span> </span>
</div> </div>
<div>
Colour:{" "}
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
</div>
<div> <div>
Category:{" "} Category:{" "}
<span className="opacity-90">{effectiveSelected?.category ?? "—"}</span> <span className="opacity-90">
{effectiveSelected?.category ?? "—"}
</span>
</div> </div>
<div> <div>
Char Ht:{" "} Char Ht:{" "}
@@ -61,18 +78,18 @@ const SightingWidgetDetails = ({
{effectiveSelected?.overviewSize ?? "—"} {effectiveSelected?.overviewSize ?? "—"}
</span> </span>
</div> </div>
{effectiveSelected?.detailsUrl ? ( </>
)}
</div>
<div className="col-span-half"> <div className="col-span-half">
<a <p
href={effectiveSelected.detailsUrl} onClick={handleDetailsClick}
target="_blank" className="underline text-blue-300 hover:cursor-pointer"
className="underline text-blue-300"
> >
Sighting Details Sighting Details
</a> </p>
</div>
) : null}
</div> </div>
</>
); );
}; };

View File

@@ -5,10 +5,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear, faListCheck } from "@fortawesome/free-solid-svg-icons"; import { faGear, faListCheck } from "@fortawesome/free-solid-svg-icons";
import type { VersionFieldType } from "../../types/types"; import type { VersionFieldType } from "../../types/types";
async function fetchVersions(signal?: AbortSignal): Promise<VersionFieldType> { async function fetchVersions(
signal?: AbortSignal
): Promise<VersionFieldType | undefined> {
try {
const res = await fetch("http://192.168.75.11/api/versions", { signal }); const res = await fetch("http://192.168.75.11/api/versions", { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json(); return res.json();
} catch (error) {
console.log(error);
return undefined;
}
} }
const pad = (n: number) => String(n).padStart(2, "0"); const pad = (n: number) => String(n).padStart(2, "0");
@@ -33,10 +40,14 @@ export default function Header() {
const ac = new AbortController(); const ac = new AbortController();
fetchVersions(ac.signal) fetchVersions(ac.signal)
.then((data) => { .then((data) => {
const serverMs = normalizeToMs(data.timeStamp); if (!data) throw new Error("No data");
const serverMs = normalizeToMs(data?.timeStamp);
setOffsetMs(serverMs - Date.now()); setOffsetMs(serverMs - Date.now());
}) })
return () => ac.abort(); .catch((err) => {
console.log(err);
});
return () => ac.abort("failed");
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@@ -69,7 +80,7 @@ export default function Header() {
<h2>Local: {localStr}</h2> <h2>Local: {localStr}</h2>
<h2>UTC: {utcStr}</h2> <h2>UTC: {utcStr}</h2>
</div> </div>
<div className="flex flex-row space-x-2">
<Link to={"/session-settings"}> <Link to={"/session-settings"}>
<FontAwesomeIcon className="text-white" icon={faListCheck} /> <FontAwesomeIcon className="text-white" icon={faListCheck} />
</Link> </Link>
@@ -78,5 +89,6 @@ export default function Header() {
</Link> </Link>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@@ -0,0 +1,26 @@
import type React from "react";
import Modal from "react-modal";
type ModalComponentProps = {
isModalOpen: boolean;
children: React.ReactNode;
};
const ModalComponent = ({
isModalOpen,
children,
close,
}: ModalComponentProps) => {
return (
<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"
overlayClassName="fixed inset-0 bg-[#1e2a38]/70 flex justify-center items-start z-100"
>
{children}
</Modal>
);
};
export default ModalComponent;

View File

@@ -29,13 +29,13 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
{side === "CameraFront" ? ( {side === "CameraFront" ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowRight} icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce" className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)} onClick={() => navigationDest(side)}
/> />
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowLeft} icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce" className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)} onClick={() => navigationDest(side)}
/> />
)} )}
@@ -47,13 +47,13 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
{side === "Front" ? ( {side === "Front" ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowLeft} icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce" className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)} onClick={() => navigationDest(side)}
/> />
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowRight} icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce" className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)} onClick={() => navigationDest(side)}
/> />
)} )}

View File

@@ -1,15 +1,17 @@
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import type { SightingWidgetType } from "../types/types"; import type { SightingType, SightingWidgetType } from "../types/types";
type SightingFeedContextType = { type SightingFeedContextType = {
sightings: (SightingWidgetType | null | undefined)[]; sightings: (SightingWidgetType | null | undefined)[];
selectedRef: number | null; selectedRef: number | null;
setSelectedRef: (ref: number | null) => void; setSelectedRef: (ref: number | null) => void;
effectiveSelected: SightingWidgetType | null; // effectiveSelected: SightingWidgetType | null;
mostRecent: SightingWidgetType | null; mostRecent: SightingWidgetType | null;
side: string; side: string;
isPending: boolean; selectedSighting: SightingType | null;
noSighting: boolean; setSelectedSighting: (sighting: SightingType | null) => void;
setSightingModalOpen: (isSightingModalOpen: boolean) => void;
isSightingModalOpen: boolean;
}; };
export const SightingFeedContext = createContext< export const SightingFeedContext = createContext<

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from "react"; import { useState, type ReactNode } from "react";
import { useSightingFeed } from "../../hooks/useSightingFeed"; import { useSightingFeed } from "../../hooks/useSightingFeed";
import { SightingFeedContext } from "../SightingFeedContext"; import { SightingFeedContext } from "../SightingFeedContext";
@@ -17,23 +17,25 @@ export const SightingFeedProvider = ({
sightings, sightings,
selectedRef, selectedRef,
setSelectedRef, setSelectedRef,
effectiveSelected, // effectiveSelected,
setSelectedSighting,
selectedSighting,
mostRecent, mostRecent,
isPending,
noSighting,
} = useSightingFeed(url); } = useSightingFeed(url);
const [isSightingModalOpen, setSightingModalOpen] = useState(false);
return ( return (
<SightingFeedContext.Provider <SightingFeedContext.Provider
value={{ value={{
sightings, sightings,
selectedRef, selectedRef,
setSelectedRef, setSelectedRef,
effectiveSelected, setSelectedSighting,
selectedSighting,
setSightingModalOpen,
isSightingModalOpen,
// effectiveSelected,
mostRecent, mostRecent,
side, side,
isPending,
noSighting,
}} }}
> >
{children} {children}

View File

@@ -0,0 +1,52 @@
// Used to fetch and load the configs for the camera side
import { useMutation, useQuery } from "@tanstack/react-query";
const base_url = import.meta.env.VITE_OUTSIDE_BASEURL;
const fetchCameraSideConfig = async ({ queryKey }) => {
const [, cameraSide] = queryKey;
const fetchUrl = `${base_url}/fetch-config?id=${cameraSide}`;
const response = await fetch(fetchUrl);
if (!response.ok) throw new Error("cannot react cameraSide ");
return response.json();
};
const updateCamerasideConfig = async (data) => {
const updateUrl = `${base_url}/update-config?id=${data.id}`;
const updateConfigPayload = {
id: data.id,
fields: [
{
property: "propLEDDriverControlURI",
value: data.friendlyName,
},
],
};
console.log(updateConfigPayload);
const response = await fetch(updateUrl, {
method: "POST",
body: JSON.stringify(updateConfigPayload),
});
if (!response.ok) throw new Error("Cannot reach update camera endpoint");
};
export const useFetchCameraConfig = (cameraSide: string) => {
const fetchedConfigQuery = useQuery({
queryKey: ["cameraSideConfig", cameraSide],
queryFn: fetchCameraSideConfig,
});
const updateConfigMutation = useMutation({
mutationKey: ["cameraSideConfigUpdate"],
mutationFn: updateCamerasideConfig,
});
return {
data: fetchedConfigQuery.data,
isPending: fetchedConfigQuery.isPending,
isError: fetchedConfigQuery.isError,
updateCameraConfig: updateConfigMutation.mutate,
};
};

View File

@@ -7,6 +7,7 @@ export const useGetConfigs = () => {
async function getConfigs() { async function getConfigs() {
try { try {
const response = await fetch(`${apiUrl}/api/config-ids`); const response = await fetch(`${apiUrl}/api/config-ids`);
if (!response.ok) { if (!response.ok) {
console.log("failed fetching"); console.log("failed fetching");
} }

View File

@@ -5,12 +5,13 @@ 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");
} }
return await response.blob(); return await response.blob();
} }

View File

@@ -35,7 +35,7 @@ async function signIn(loginDetails: NPEDFieldType) {
{ property: "propClientID", value: clientId }, { property: "propClientID", value: clientId },
], ],
}; };
console.log(frontId);
const frontCameraResponse = await fetch(NPEDLoginURLFront, { const frontCameraResponse = await fetch(NPEDLoginURLFront, {
method: "POST", method: "POST",
body: JSON.stringify(frontCameraPayload), body: JSON.stringify(frontCameraPayload),

View File

@@ -1,7 +1,10 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { SightingWidgetType } from "../types/types"; import type { SightingType, SightingWidgetType } from "../types/types";
async function fetchSighting(url: string, ref: number): Promise<SightingWidgetType> { async function fetchSighting(
url: string,
ref: number
): Promise<SightingWidgetType> {
const res = await fetch(`${url}${ref}`); const res = await fetch(`${url}${ref}`);
if (!res.ok) throw new Error(String(res.status)); if (!res.ok) throw new Error(String(res.status));
return await res.json(); return await res.json();
@@ -11,6 +14,9 @@ export function useSightingFeed(url: string) {
const [sightings, setSightings] = useState<SightingWidgetType[]>([]); const [sightings, setSightings] = useState<SightingWidgetType[]>([]);
const [selectedRef, setSelectedRef] = useState<number | null>(null); const [selectedRef, setSelectedRef] = useState<number | null>(null);
const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null); const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null);
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(
null
);
const currentRef = useRef<number>(-1); const currentRef = useRef<number>(-1);
const pollingTimeout = useRef<ReturnType<typeof setTimeout> | null>(null); const pollingTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -35,7 +41,7 @@ export function useSightingFeed(url: string) {
currentRef.current = data.ref; currentRef.current = data.ref;
lastValidTimestamp.current = now; lastValidTimestamp.current = now;
setSightings(prev => { setSightings((prev) => {
const updated = [data, ...prev].slice(0, 7); const updated = [data, ...prev].slice(0, 7);
return updated; return updated;
}); });
@@ -58,13 +64,15 @@ export function useSightingFeed(url: string) {
}; };
}, [url]); }, [url]);
const selected = sightings.find(s => s?.ref === selectedRef) ?? mostRecent; // const selected = sightings.find(s => s?.ref === selectedRef) ?? mostRecent;
return { return {
sightings, sightings,
selectedRef, selectedRef,
setSelectedRef, setSelectedRef,
mostRecent, mostRecent,
effectiveSelected: selected, setSelectedSighting,
selectedSighting,
// effectiveSelected: selected,
}; };
} }

View File

@@ -1,7 +1,7 @@
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard"; import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard"; import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard";
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget"; import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
import ModalComponent from "../components/UI/ModalComponent";
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider"; import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
const Dashboard = () => { const Dashboard = () => {
@@ -9,16 +9,23 @@ const Dashboard = () => {
<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">
<SightingFeedProvider <SightingFeedProvider
url={ url={
"http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef=" "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
// "http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef="
} }
side="Front" side="Front"
> >
<FrontCameraOverviewCard className="order-1" /> <FrontCameraOverviewCard className="order-1" />
<SightingHistoryWidget className="order-3" /> <SightingHistoryWidget className="order-5" />
<ModalComponent>
<div className="text-black">Hello</div>
</ModalComponent>
</SightingFeedProvider> </SightingFeedProvider>
<SightingFeedProvider <SightingFeedProvider
url="http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef=" url={
"http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
// "http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef="
}
side="Rear" side="Rear"
> >
<RearCameraOverviewCard className="order-2" /> <RearCameraOverviewCard className="order-2" />

View File

@@ -21,7 +21,7 @@ const FrontCamera = () => {
side="CameraFront" side="CameraFront"
settingsPage={true} settingsPage={true}
/> />
<CameraSettings title="Front Camera Settings" /> <CameraSettings title="Front Camera Settings" side="CameraFront" />
<Toaster /> <Toaster />
</div> </div>
); );

View File

@@ -16,7 +16,7 @@ const RearCamera = () => {
className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-4 px-2 sm:px-4 lg:px-0 w-full order-first" className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-4 px-2 sm:px-4 lg:px-0 w-full order-first"
{...handlers} {...handlers}
> >
<CameraSettings title="Rear Camera Settings" /> <CameraSettings title="Rear Camera Settings" side={"CameraRear"} />
<OverviewVideoContainer <OverviewVideoContainer
title={"Rear Camera"} title={"Rear Camera"}
side={"CameraRear"} side={"CameraRear"}

View File

@@ -34,7 +34,7 @@ export type CameraSettingValues = {
cameraAddress: string; cameraAddress: string;
userName: string; userName: string;
password: string; password: string;
setupCamera: number; id: number;
}; };
export type CameraSettingErrorValues = Partial< export type CameraSettingErrorValues = Partial<
@@ -113,7 +113,6 @@ export type VersionFieldType = {
"Model No.": string; "Model No.": string;
}; };
export type Metadata = { export type Metadata = {
npedJSON: NpedJSON; npedJSON: NpedJSON;
"hotlist-matches": HotlistMatches; "hotlist-matches": HotlistMatches;