Files
Mav-Mobile-UI/src/components/SightingsWidget/SightingWidget.tsx

168 lines
5.2 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2025-09-16 14:20:38 +01:00
import type { SightingType } from "../../types/types";
import { BLANK_IMG } from "../../utils/utils";
2025-08-20 08:27:05 +01:00
import NumberPlate from "../PlateStack/NumberPlate";
import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
import clsx from "clsx";
import { useSightingFeedContext } from "../../context/SightingFeedContext";
2025-09-12 08:21:52 +01:00
import SightingModal from "../SightingModal/SightingModal";
// import { useAlertHitContext } from "../../context/AlertHitContext";
import InfoBar from "./InfoBar";
2025-08-20 08:27:05 +01:00
function useNow(tickMs = 1000) {
const [, setNow] = useState(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), tickMs);
return () => clearInterval(id);
}, [tickMs]);
return undefined;
}
2025-09-15 10:27:31 +01:00
type SightingHistoryProps = {
baseUrl?: string;
2025-08-20 08:27:05 +01:00
entries?: number; // number of rows to show
pollMs?: number; // poll frequency
autoSelectLatest?: boolean;
2025-09-15 10:27:31 +01:00
title: string;
className: React.HTMLAttributes<HTMLDivElement> | string;
2025-08-20 08:27:05 +01:00
};
2025-09-15 10:27:31 +01:00
// /type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>;
2025-08-20 08:27:05 +01:00
export default function SightingHistoryWidget({
className,
2025-09-15 10:27:31 +01:00
title,
}: SightingHistoryProps) {
2025-08-20 08:27:05 +01:00
useNow(1000);
2025-09-12 08:21:52 +01:00
const {
sightings,
setSelectedSighting,
setSightingModalOpen,
isSightingModalOpen,
selectedSighting,
} = useSightingFeedContext();
2025-08-20 08:27:05 +01:00
// const { dispatch } = useAlertHitContext();
const hasAutoOpenedRef = useRef(false);
2025-08-20 08:27:05 +01:00
const onRowClick = useCallback(
2025-09-16 14:20:38 +01:00
(sighting: SightingType) => {
2025-09-12 08:21:52 +01:00
if (!sighting) return;
setSightingModalOpen(!isSightingModalOpen);
setSelectedSighting(sighting);
2025-08-20 08:27:05 +01:00
},
2025-09-12 08:21:52 +01:00
[isSightingModalOpen, setSelectedSighting, setSightingModalOpen]
2025-08-20 08:27:05 +01:00
);
2025-08-20 08:27:05 +01:00
const rows = useMemo(
2025-09-16 14:20:38 +01:00
() => sightings?.filter(Boolean) as SightingType[],
2025-08-22 10:38:28 +01:00
[sightings]
2025-08-20 08:27:05 +01:00
);
// useEffect(() => {
// rows?.forEach((obj) => {
// const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 404;
// if (isNPEDHit) {
// dispatch({
// type: "ADD",
// payload: obj,
// });
// }
// });
// }, [rows, dispatch]);
useEffect(() => {
if (hasAutoOpenedRef.current) return;
const firstHot = rows?.find(
(r) => r?.metadata?.hotlistMatches?.Hotlist0 === true
);
if (firstHot) {
setSelectedSighting(firstHot);
setSightingModalOpen(true);
hasAutoOpenedRef.current = true; // prevent future auto-opens
}
}, [rows, setSelectedSighting, setSightingModalOpen]);
2025-09-12 08:21:52 +01:00
const handleClose = () => {
setSightingModalOpen(false);
};
2025-08-20 08:27:05 +01:00
return (
2025-09-12 08:21:52 +01:00
<>
<Card className={clsx("overflow-y-auto h-100", className)}>
2025-09-15 10:27:31 +01:00
<CardHeader title={title} />
2025-09-12 08:21:52 +01:00
<div className="flex flex-col gap-3 ">
{/* Rows */}
<div className="flex flex-col">
{rows?.map((obj, idx) => {
// const isNPEDHit = obj?.metadata?.npedJSON?.status_code === 404;
2025-09-12 08:21:52 +01:00
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const primaryIsColour = obj?.srcCam === 1;
const secondaryMissing = (obj?.vrmSecondary ?? "") === "";
2025-09-12 08:21:52 +01:00
return (
2025-08-29 14:55:37 +01:00
<div
2025-09-12 08:21:52 +01:00
key={idx}
className={`border border-neutral-700 rounded-md mb-2 p-2 cursor-pointer`}
onClick={() => onRowClick(obj)}
2025-08-29 14:55:37 +01:00
>
2025-09-12 08:21:52 +01:00
{/* Info bar */}
<InfoBar obj={obj} />
2025-09-12 08:21:52 +01:00
{/* Patch row */}
2025-08-20 08:27:05 +01:00
<div
className={`flex items-center gap-3 mt-2 justify-between
2025-09-12 08:21:52 +01:00
`}
2025-08-20 08:27:05 +01:00
>
{obj?.plateUrlInfrared && (
<div
className={`border p-1 ${
primaryIsColour ? "" : "ring-2 ring-lime-400"
} ${!obj ? "opacity-30" : ""}`}
>
<img
src={obj?.plateUrlInfrared || BLANK_IMG}
height={48}
alt="infrared patch"
className={!primaryIsColour ? "" : "opacity-60"}
/>
</div>
)}
2025-09-12 08:21:52 +01:00
<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} />
2025-08-20 08:27:05 +01:00
</div>
</div>
2025-09-12 08:21:52 +01:00
);
})}
</div>
2025-08-20 08:27:05 +01:00
</div>
2025-09-12 08:21:52 +01:00
</Card>
<SightingModal
isSightingModalOpen={isSightingModalOpen}
handleClose={handleClose}
sighting={selectedSighting}
/>
</>
2025-08-20 08:27:05 +01:00
);
}