added sighting feed
This commit is contained in:
121
src/components/SightingsWidget/SightingWidget.tsx
Normal file
121
src/components/SightingsWidget/SightingWidget.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { 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";
|
||||
|
||||
function useNow(tickMs = 1000) {
|
||||
const [, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), tickMs);
|
||||
return () => clearInterval(id);
|
||||
}, [tickMs]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export type SightingHistoryProps = {
|
||||
baseUrl: string;
|
||||
entries?: number; // number of rows to show
|
||||
pollMs?: number; // poll frequency
|
||||
autoSelectLatest?: boolean;
|
||||
};
|
||||
|
||||
type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export default function SightingHistoryWidget({
|
||||
className,
|
||||
}: SightingHistoryWidgetProps) {
|
||||
useNow(1000);
|
||||
const { items, selectedRef, setSelectedRef } = useSightingFeedContext();
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(ref: number) => {
|
||||
setSelectedRef(ref);
|
||||
},
|
||||
[setSelectedRef]
|
||||
);
|
||||
|
||||
const rows = useMemo(
|
||||
() => items.filter(Boolean) as SightingWidgetType[],
|
||||
[items]
|
||||
);
|
||||
|
||||
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 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 */}
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
93
src/components/SightingsWidget/SightingWidgetDetails.tsx
Normal file
93
src/components/SightingsWidget/SightingWidgetDetails.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { SightingWidgetType } from "../../types/types";
|
||||
|
||||
type SightingWidgetDetailsProps = {
|
||||
effectiveSelected: SightingWidgetType | null;
|
||||
};
|
||||
|
||||
const SightingWidgetDetails = ({
|
||||
effectiveSelected,
|
||||
}: SightingWidgetDetailsProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
||||
<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>
|
||||
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>
|
||||
Motion:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.motion ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Seen:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.seenCount ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Location:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.locationName ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Lane:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.laneID ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Radar:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.radarSpeed ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Track:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.trackSpeed ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
{effectiveSelected?.detailsUrl ? (
|
||||
<div className="col-span-full">
|
||||
<a
|
||||
href={effectiveSelected.detailsUrl}
|
||||
target="_blank"
|
||||
className="underline text-blue-300"
|
||||
>
|
||||
Sighting Details
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SightingWidgetDetails;
|
||||
Reference in New Issue
Block a user