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??
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?
obscure details on dashboard to a toggle
FYI:

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<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" />
<title>MAV | In Car System</title>
</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";
import { toast } from "sonner";
const CameraSettingFields = () => {
const CameraSettingFields = ({ initialData, updateCameraConfig }) => {
const initialValues: CameraSettingValues = {
friendlyName: "",
friendlyName: initialData?.propLEDDriverControlURI?.value,
cameraAddress: "",
userName: "",
password: "",
setupCamera: 1,
id: initialData?.id,
};
const validateValues = (values: CameraSettingValues) => {
@@ -29,7 +29,7 @@ const CameraSettingFields = () => {
const handleSubmit = (values: CameraSettingValues) => {
// post values to endpoint
console.log(values);
updateCameraConfig(values);
toast("Settings Saved");
};

View File

@@ -1,15 +1,28 @@
import { useFetchCameraConfig } from "../../hooks/useCameraConfig";
import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
import CameraSettingFields from "./CameraSettingFields";
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 (
<Card>
<div className="relative flex flex-col space-y-3 h-full">
<CardHeader title={title} icon={faWrench} />
<CameraSettingFields />
</div>
{isError && <>Cannot Fetch camera config</>}
{isPending ? (
<>Loading</>
) : (
<div className="relative flex flex-col space-y-3 h-full">
<CardHeader title={title} icon={faWrench} />
<CameraSettingFields
initialData={data}
updateCameraConfig={updateCameraConfig}
/>
</div>
)}
</Card>
);
};

View File

@@ -1,74 +1,88 @@
export async function handleSystemSave(deviceName: string, sntpServer: string, sntpInterval: number, timeZone: string) {
const payload = { // Build JSON
id: "GLOBAL--Device",
fields: [
{ property: "propDeviceName", value: deviceName },
{ property: "propSNTPServer", value: sntpServer },
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
{ property: "propLocalTimeZone", value: timeZone }
]
};
export async function handleSystemSave(
deviceName: string,
sntpServer: string,
sntpInterval: number,
timeZone: string
) {
const payload = {
// Build JSON
id: "GLOBAL--Device",
fields: [
{ property: "propDeviceName", value: deviceName },
{ property: "propSNTPServer", value: sntpServer },
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
{ property: "propLocalTimeZone", value: timeZone },
],
};
try {
const response = await fetch("http://192.168.75.11/api/update-config", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(payload)
});
try {
const response = await fetch("http://192.168.75.11/api/update-config", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
}
alert("System Settings Saved Successfully!");
} catch (err) {
console.error(err);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`HTTP ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`
);
}
alert("System Settings Saved Successfully!");
} catch (err) {
console.error(err);
}
}
export async function handleSystemRecall() {
const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device";
const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device";
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 7000);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 7000);
try {
const response = await fetch(url, {
method: "GET",
headers: { "Accept": "application/json" },
signal: controller.signal
});
try {
const response = await fetch(url, {
method: "GET",
headers: { Accept: "application/json" },
signal: controller.signal,
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
}
const data = await response.json();
const deviceName = data?.propDeviceName?.value ?? null;
const sntpServer = data?.propSNTPServer?.value ?? null;
const timeZone = data?.propLocalTimeZone?.value ?? null;
let sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value;
let sntpInterval =
typeof sntpIntervalRaw === "number"
? sntpIntervalRaw
: Number.parseInt(String(sntpIntervalRaw).trim(), 10);
if (!Number.isFinite(sntpInterval)) {
sntpInterval = 60;
}
return { deviceName, sntpServer, sntpInterval, timeZone };
} catch (err) {
console.error(err);
return null;
} finally {
clearTimeout(timeoutId);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`HTTP ${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`
);
}
const data = await response.json();
const deviceName = data?.propDeviceName?.value ?? null;
const sntpServer = data?.propSNTPServer?.value ?? null;
const timeZone = data?.propLocalTimeZone?.value ?? null;
const sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value;
let sntpInterval =
typeof sntpIntervalRaw === "number"
? sntpIntervalRaw
: Number.parseInt(String(sntpIntervalRaw).trim(), 10);
if (!Number.isFinite(sntpInterval)) {
sntpInterval = 60;
}
return { deviceName, sntpServer, sntpInterval, timeZone };
} catch (err) {
console.error(err);
return null;
} finally {
clearTimeout(timeoutId);
}
}

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

View File

@@ -1,11 +1,12 @@
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 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";
function useNow(tickMs = 1000) {
const [, setNow] = useState(() => Date.now());
@@ -29,97 +30,112 @@ export default function SightingHistoryWidget({
className,
}: SightingHistoryWidgetProps) {
useNow(1000);
const { sightings, selectedRef, setSelectedRef } = useSightingFeedContext();
const {
sightings,
setSelectedSighting,
setSightingModalOpen,
isSightingModalOpen,
selectedSighting,
} = useSightingFeedContext();
const onRowClick = useCallback(
(ref: number) => {
setSelectedRef(ref);
(sighting: SightingType) => {
if (!sighting) return;
setSightingModalOpen(!isSightingModalOpen);
setSelectedSighting(sighting);
},
[setSelectedRef]
[isSightingModalOpen, setSelectedSighting, setSightingModalOpen]
);
const rows = useMemo(
() => sightings?.filter(Boolean) as SightingWidgetType[],
[sightings]
);
const handleClose = () => {
setSightingModalOpen(false);
};
return (
<Card className={clsx("overflow-y-auto h-100", className)}>
<CardHeader title="Front Camera Sightings" />
<div className="flex flex-col gap-3 ">
{/* Rows */}
<div className="flex flex-col">
{rows?.map((obj, idx) => {
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 201;
const isSelected = obj?.ref === selectedRef;
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const primaryIsColour = obj?.srcCam === 1;
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
return (
<div
key={idx}
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer ${
isSelected ? "ring-2 ring-blue-400" : ""
}`}
onClick={() => onRowClick(obj.ref)}
>
{/* Info bar */}
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
<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>
{/* Patch row */}
<>
<Card className={clsx("overflow-y-auto h-100", className)}>
<CardHeader title="Front Camera Sightings" />
<div className="flex flex-col gap-3 ">
{/* Rows */}
<div className="flex flex-col">
{rows?.map((obj, idx) => {
const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 201;
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const primaryIsColour = obj?.srcCam === 1;
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
console.log(obj);
return (
<div
className={`flex items-center gap-3 mt-2
key={idx}
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer`}
onClick={() => onRowClick(obj)}
>
{/* Info bar */}
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded">
<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>
{/* Patch row */}
<div
className={`flex items-center gap-3 mt-2
${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
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" : ""
} ${
secondaryMissing && primaryIsColour
? "opacity-30 grayscale"
: ""
}`}
>
<img
src={obj?.plateUrlColour || BLANK_IMG}
height={48}
alt="colour patch"
className={primaryIsColour ? "" : "opacity-60"}
/>
</div>
<NumberPlate motion={motionAway} vrm={obj?.vrm} />
</div>
<div
className={`border p-1 ${
primaryIsColour ? "ring-2 ring-lime-400" : ""
} ${
secondaryMissing && primaryIsColour
? "opacity-30 grayscale"
: ""
}`}
>
<img
src={obj?.plateUrlColour || BLANK_IMG}
height={48}
alt="colour patch"
className={primaryIsColour ? "" : "opacity-60"}
/>
</div>
<NumberPlate motion={motionAway} vrm={obj?.vrm} />
</div>
</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 { useState } from "react";
type SightingWidgetDetailsProps = {
effectiveSelected: SightingWidgetType | null;
@@ -7,72 +8,88 @@ type SightingWidgetDetailsProps = {
const SightingWidgetDetails = ({
effectiveSelected,
}: SightingWidgetDetailsProps) => {
const [advancedDetailsEnabled, setAdvancedDetailsEnabled] = useState(false);
const handleDetailsClick = () =>
setAdvancedDetailsEnabled(!advancedDetailsEnabled);
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
<div>
VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? ""}</span>
</div>
<div>
Timestamp:{" "}
<span className="opacity-90">{effectiveSelected?.timeStamp ?? "—"}</span>
</div>
<div>
Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
</div>
<div>
Model:{" "}
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
</div>
<div>
Country:{" "}
<span className="opacity-90">{effectiveSelected?.countryCode ?? "—"}</span>
</div>
<div>
Seen:{" "}
<span className="opacity-90">
{effectiveSelected?.seenCount ?? "—"}
</span>
</div>
<div>
Colour:{" "}
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
</div>
<div>
Category:{" "}
<span className="opacity-90">{effectiveSelected?.category ?? "—"}</span>
</div>
<div>
Char Ht:{" "}
<span className="opacity-90">
{effectiveSelected?.charHeight ?? "—"}
</span>
</div>
<div>
Plate Size:{" "}
<span className="opacity-90">
{effectiveSelected?.plateSize ?? "—"}
</span>
</div>
<div>
Overview Size:{" "}
<span className="opacity-90">
{effectiveSelected?.overviewSize ?? "—"}
</span>
</div>
{effectiveSelected?.detailsUrl ? (
<div className="col-span-half">
<a
href={effectiveSelected.detailsUrl}
target="_blank"
className="underline text-blue-300"
>
Sighting Details
</a>
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
<div>
VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? "—"}</span>
</div>
) : null}
</div>
<div>
Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
</div>
<div>
Model:{" "}
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
</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>
Country:{" "}
<span className="opacity-90">
{effectiveSelected?.countryCode ?? "—"}
</span>
</div>
<div>
Seen:{" "}
<span className="opacity-90">
{effectiveSelected?.seenCount ?? "—"}
</span>
</div>
<div>
Category:{" "}
<span className="opacity-90">
{effectiveSelected?.category ?? "—"}
</span>
</div>
<div>
Char Ht:{" "}
<span className="opacity-90">
{effectiveSelected?.charHeight ?? "—"}
</span>
</div>
<div>
Plate Size:{" "}
<span className="opacity-90">
{effectiveSelected?.plateSize ?? "—"}
</span>
</div>
<div>
Overview Size:{" "}
<span className="opacity-90">
{effectiveSelected?.overviewSize ?? "—"}
</span>
</div>
</>
)}
</div>
<div className="col-span-half">
<p
onClick={handleDetailsClick}
className="underline text-blue-300 hover:cursor-pointer"
>
Sighting Details
</p>
</div>
</>
);
};

View File

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

View File

@@ -1,15 +1,17 @@
import { createContext, useContext } from "react";
import type { SightingWidgetType } from "../types/types";
import type { SightingType, SightingWidgetType } from "../types/types";
type SightingFeedContextType = {
sightings: (SightingWidgetType | null | undefined)[];
selectedRef: number | null;
setSelectedRef: (ref: number | null) => void;
effectiveSelected: SightingWidgetType | null;
// effectiveSelected: SightingWidgetType | null;
mostRecent: SightingWidgetType | null;
side: string;
isPending: boolean;
noSighting: boolean;
selectedSighting: SightingType | null;
setSelectedSighting: (sighting: SightingType | null) => void;
setSightingModalOpen: (isSightingModalOpen: boolean) => void;
isSightingModalOpen: boolean;
};
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 { SightingFeedContext } from "../SightingFeedContext";
@@ -17,23 +17,25 @@ export const SightingFeedProvider = ({
sightings,
selectedRef,
setSelectedRef,
effectiveSelected,
// effectiveSelected,
setSelectedSighting,
selectedSighting,
mostRecent,
isPending,
noSighting,
} = useSightingFeed(url);
const [isSightingModalOpen, setSightingModalOpen] = useState(false);
return (
<SightingFeedContext.Provider
value={{
sightings,
selectedRef,
setSelectedRef,
effectiveSelected,
setSelectedSighting,
selectedSighting,
setSightingModalOpen,
isSightingModalOpen,
// effectiveSelected,
mostRecent,
side,
isPending,
noSighting,
}}
>
{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() {
try {
const response = await fetch(`${apiUrl}/api/config-ids`);
if (!response.ok) {
console.log("failed fetching");
}

View File

@@ -5,12 +5,13 @@ const apiUrl = import.meta.env.VITE_BASEURL;
async function fetchSnapshot(cameraSide: string) {
const response = await fetch(
// `http://100.116.253.81/Colour-preview`
`${apiUrl}/${cameraSide}-preview`
`http://100.116.253.81/Colour-preview`
// `${apiUrl}/${cameraSide}-preview`
);
if (!response.ok) {
throw new Error("Cannot reach endpoint");
}
return await response.blob();
}

View File

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

View File

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

View File

@@ -1,24 +1,31 @@
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard";
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
import ModalComponent from "../components/UI/ModalComponent";
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
const Dashboard = () => {
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">
<SightingFeedProvider
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"
>
<FrontCameraOverviewCard className="order-1" />
<SightingHistoryWidget className="order-3" />
<SightingHistoryWidget className="order-5" />
<ModalComponent>
<div className="text-black">Hello</div>
</ModalComponent>
</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"
>
<RearCameraOverviewCard className="order-2" />

View File

@@ -21,7 +21,7 @@ const FrontCamera = () => {
side="CameraFront"
settingsPage={true}
/>
<CameraSettings title="Front Camera Settings" />
<CameraSettings title="Front Camera Settings" side="CameraFront" />
<Toaster />
</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"
{...handlers}
>
<CameraSettings title="Rear Camera Settings" />
<CameraSettings title="Rear Camera Settings" side={"CameraRear"} />
<OverviewVideoContainer
title={"Rear Camera"}
side={"CameraRear"}

View File

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