code fixes and adding modal
This commit is contained in:
@@ -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");
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
32
src/components/SightingModal/SightingModal.tsx
Normal file
32
src/components/SightingModal/SightingModal.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
26
src/components/UI/ModalComponent.tsx
Normal file
26
src/components/UI/ModalComponent.tsx
Normal 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;
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user