feat: add modal component for sighting details with content display
- Implemented ModalComponent for reusable modal functionality. - Created SightingItemModal to manage modal state and display sighting details. - Developed SightingModalContent to render sighting information including video feed and metadata.
This commit is contained in:
@@ -7,7 +7,6 @@ type PlateReadProps = {
|
||||
};
|
||||
|
||||
const PlateRead = ({ sighting }: PlateReadProps) => {
|
||||
console.log(sighting);
|
||||
const vrm = sighting?.vrm;
|
||||
const region = sighting?.laneID;
|
||||
const timestamp = sighting?.timeStamp;
|
||||
|
||||
@@ -4,25 +4,34 @@ import NumberPlate from "../platePatch/NumberPlate";
|
||||
|
||||
type SightingItemProps = {
|
||||
sighting: SightingType;
|
||||
onOpenModal: () => void;
|
||||
};
|
||||
|
||||
const SightingItem = ({ sighting }: SightingItemProps) => {
|
||||
const SightingItem = ({ sighting, onOpenModal }: SightingItemProps) => {
|
||||
const motion = sighting.motion.toLowerCase() === "away" ? true : false;
|
||||
|
||||
const timeStamp = timeAgo(sighting.timeStampMillis);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center border p-2 mb-2 rounded-lg border-gray-500 justify-between hover:bg-[#233241] hover:cursor-pointer">
|
||||
<div>
|
||||
<>
|
||||
<div
|
||||
className="flex flex-row items-center border p-2 mb-2 rounded-lg border-gray-500 justify-between hover:bg-[#233241] hover:cursor-pointer"
|
||||
onClick={onOpenModal}
|
||||
>
|
||||
<div>
|
||||
<span className="font-light border bg-blue-400 text-blue-800 px-2 rounded">{timeStamp}</span>
|
||||
</div>
|
||||
<div className="text-xl">
|
||||
<span className="font-semibold text-gray-200">{sighting.vrm}</span>
|
||||
<div>
|
||||
<span className="font-light border bg-blue-400 text-blue-800 px-2 rounded">{timeStamp}</span>
|
||||
</div>
|
||||
<div className="text-xl">
|
||||
<span className="font-semibold text-gray-200">{sighting.vrm}</span>
|
||||
</div>
|
||||
</div>
|
||||
{window.innerWidth > 768 ? (
|
||||
<NumberPlate vrm={sighting.vrm} motion={motion} size="md" />
|
||||
) : (
|
||||
<NumberPlate vrm={sighting.vrm} motion={motion} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
<NumberPlate vrm={sighting.vrm} motion={motion} size="md" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
import { useState } from "react";
|
||||
import CardHeader from "../../../../components/CardHeader";
|
||||
import Card from "../../../../components/ui/Card";
|
||||
import type { SightingType } from "../../../../utils/types";
|
||||
import SightingItem from "./SightingItem";
|
||||
import SightingItemModal from "./sightingItemModal/SightingItemModal";
|
||||
|
||||
type SightingStackProps = {
|
||||
sightings: SightingType[];
|
||||
};
|
||||
const SightingStack = ({ sightings }: SightingStackProps) => {
|
||||
const [isSightingModalOpen, setIsSightingModalOpen] = useState(false);
|
||||
const [currentSighting, setCurrentSighting] = useState<SightingType | null>(null);
|
||||
const handleOpenModal = (sighting: SightingType | null) => {
|
||||
if (!sighting) return;
|
||||
setCurrentSighting(sighting);
|
||||
setIsSightingModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4 w-full h-full ">
|
||||
<CardHeader title="Live Sightings" />
|
||||
<div className="md:h-[65%]">
|
||||
{sightings.map((sighting) => (
|
||||
<SightingItem key={sighting.ref} sighting={sighting} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<>
|
||||
<Card className="p-4 w-full h-full ">
|
||||
<CardHeader title="Live Sightings" />
|
||||
<div className="md:h-[65%]">
|
||||
{sightings.map((sighting) => (
|
||||
<SightingItem key={sighting.ref} sighting={sighting} onOpenModal={() => handleOpenModal(sighting)} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<SightingItemModal
|
||||
isOpen={isSightingModalOpen}
|
||||
close={() => setIsSightingModalOpen(false)}
|
||||
sighting={currentSighting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import ModalComponent from "../../../../../components/ui/ModalComponent";
|
||||
import type { SightingType } from "../../../../../utils/types";
|
||||
import SightingModalContent from "./SightingModalContent";
|
||||
|
||||
type SightingItemModalProps = {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
sighting: SightingType | null;
|
||||
};
|
||||
const SightingItemModal = ({ isOpen, close, sighting }: SightingItemModalProps) => {
|
||||
return (
|
||||
<ModalComponent isModalOpen={isOpen} close={close}>
|
||||
<SightingModalContent sighting={sighting} />
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SightingItemModal;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useCameraSettingsContext } from "../../../../../app/context/CameraSettingsContext";
|
||||
import type { SightingType } from "../../../../../utils/types";
|
||||
import NumberPlate from "../../platePatch/NumberPlate";
|
||||
import VideoFeed from "../../videoFeed/VideoFeed";
|
||||
|
||||
type SightingModalContentProps = {
|
||||
sighting: SightingType | null;
|
||||
};
|
||||
|
||||
const SightingModalContent = ({ sighting }: SightingModalContentProps) => {
|
||||
const { state: cameraSettings } = useCameraSettingsContext();
|
||||
const size = cameraSettings.imageSize;
|
||||
const modalImageSize = { width: size.width / 1.5, height: size.height / 1.5 };
|
||||
return (
|
||||
<div>
|
||||
{sighting ? (
|
||||
<>
|
||||
<div className="flex flex-row items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">Sighting Details</h2>
|
||||
<NumberPlate vrm={sighting.vrm} motion={sighting.motion.toLowerCase() === "away"} size="md" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<VideoFeed mostRecentSighting={sighting} isLoading={false} size={modalImageSize} isModal={true} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">VRM</p>
|
||||
<p className="text-lg font-semibold">{sighting.vrm}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Timestamp</p>
|
||||
<p className="text-sm">{new Date(sighting.timeStampMillis).toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Motion</p>
|
||||
<p className="text-lg font-semibold">{sighting.motion}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>No sighting data available.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SightingModalContent;
|
||||
@@ -7,26 +7,36 @@ import { useCameraSettingsContext } from "../../../../app/context/CameraSettings
|
||||
type VideoFeedProps = {
|
||||
mostRecentSighting: SightingType;
|
||||
isLoading: boolean;
|
||||
size: { width: number; height: number };
|
||||
|
||||
modeSetting?: number;
|
||||
isModal?: boolean;
|
||||
};
|
||||
|
||||
const VideoFeed = ({ mostRecentSighting, isLoading }: VideoFeedProps) => {
|
||||
const VideoFeed = ({ mostRecentSighting, isLoading, size, modeSetting, isModal = false }: VideoFeedProps) => {
|
||||
const { state: cameraSettings, dispatch } = useCameraSettingsContext();
|
||||
|
||||
const mode = cameraSettings.mode;
|
||||
const [size, setSize] = useState<{ width: number; height: number }>({ width: 1280, height: 960 });
|
||||
|
||||
const contextMode = cameraSettings.mode;
|
||||
const [localMode, setLocalMode] = useState(0);
|
||||
const mode = isModal ? localMode : contextMode;
|
||||
const { image, plateRect, plateTrack } = useCreateVideoSnapshot(mostRecentSighting);
|
||||
|
||||
const handleModeChange = (newMode: number) => {
|
||||
if (newMode > 2) dispatch({ type: "SET_MODE", payload: 0 });
|
||||
else dispatch({ type: "SET_MODE", payload: newMode });
|
||||
if (modeSetting) return;
|
||||
|
||||
const nextMode = newMode > 2 ? 0 : newMode;
|
||||
|
||||
if (isModal) {
|
||||
setLocalMode(nextMode);
|
||||
} else {
|
||||
dispatch({ type: "SET_MODE", payload: nextMode });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateSize = () => {
|
||||
const width = window.innerWidth * 0.48;
|
||||
const height = (width * 2) / 3;
|
||||
setSize({ width, height });
|
||||
dispatch({ type: "SET_IMAGE_SIZE", payload: { width, height } });
|
||||
};
|
||||
updateSize();
|
||||
window.addEventListener("resize", updateSize);
|
||||
@@ -39,20 +49,22 @@ const VideoFeed = ({ mostRecentSighting, isLoading }: VideoFeedProps) => {
|
||||
<div className="w-[70%] mt-[2%]">
|
||||
<Stage width={size.width} height={size.height} onClick={() => handleModeChange(mode + 1)}>
|
||||
<Layer>
|
||||
<Image
|
||||
image={image}
|
||||
height={size.height}
|
||||
width={size.width}
|
||||
onMouseEnter={(e) => {
|
||||
const container = e.target.getStage()?.container();
|
||||
if (container) container.style.cursor = "pointer";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const container = e.target.getStage()?.container();
|
||||
if (container) container.style.cursor = "default";
|
||||
}}
|
||||
cornerRadius={10}
|
||||
/>
|
||||
{image && (
|
||||
<Image
|
||||
image={image}
|
||||
height={size.height}
|
||||
width={size.width}
|
||||
onMouseEnter={(e) => {
|
||||
const container = e.target.getStage()?.container();
|
||||
if (container) container.style.cursor = "pointer";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const container = e.target.getStage()?.container();
|
||||
if (container) container.style.cursor = "default";
|
||||
}}
|
||||
cornerRadius={10}
|
||||
/>
|
||||
)}
|
||||
</Layer>
|
||||
{plateRect && mode === 1 && (
|
||||
<Layer>
|
||||
|
||||
Reference in New Issue
Block a user