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

@@ -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>
</>
);
};